diff options
Diffstat (limited to 'resources/src/mediawiki/mediawiki.js')
-rw-r--r-- | resources/src/mediawiki/mediawiki.js | 862 |
1 files changed, 493 insertions, 369 deletions
diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js index e29c734d..ee57c21f 100644 --- a/resources/src/mediawiki/mediawiki.js +++ b/resources/src/mediawiki/mediawiki.js @@ -1,7 +1,7 @@ /** * Base library for MediaWiki. * - * Exposed as globally as `mediaWiki` with `mw` as shortcut. + * Exposed globally as `mediaWiki` with `mw` as shortcut. * * @class mw * @alternateClassName mediaWiki @@ -10,8 +10,6 @@ ( function ( $ ) { 'use strict'; - /* Private Members */ - var mw, hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice, @@ -19,87 +17,104 @@ trackQueue = []; /** - * Log a message to window.console, if possible. Useful to force logging of some - * errors that are otherwise hard to detect (I.e., this logs also in production mode). - * Gets console references in each invocation, so that delayed debugging tools work - * fine. No need for optimization here, which would only result in losing logs. - * - * @private - * @method log_ - * @param {string} msg text for the log entry. - * @param {Error} [e] - */ - function log( msg, e ) { - var console = window.console; - if ( console && console.log ) { - console.log( msg ); - // If we have an exception object, log it through .error() to trigger - // proper stacktraces in browsers that support it. There are no (known) - // browsers that don't support .error(), that do support .log() and - // have useful exception handling through .log(). - if ( e && console.error ) { - console.error( String( e ), e ); - } - } - } - - /* Object constructors */ - - /** - * Creates an object that can be read from or written to from prototype functions - * that allow both single and multiple variables at once. + * Create an object that can be read from or written to from methods that allow + * interaction both with single and multiple properties at once. * * @example * - * var addies, wanted, results; + * var collection, query, results; * * // Create your address book - * addies = new mw.Map(); + * collection = new mw.Map(); * * // This data could be coming from an external source (eg. API/AJAX) - * addies.set( { - * 'John Doe' : '10 Wall Street, New York, USA', - * 'Jane Jackson' : '21 Oxford St, London, UK', - * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL' + * collection.set( { + * 'John Doe': 'john@example.org', + * 'Jane Doe': 'jane@example.org', + * 'George van Halen': 'gvanhalen@example.org' * } ); * - * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson']; + * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson']; * * // You can detect missing keys first - * if ( !addies.exists( wanted ) ) { - * // One or more are missing (in this case: "George Johnson") + * if ( !collection.exists( wanted ) ) { + * // One or more are missing (in this case: "Daniel Jackson") * mw.log( 'One or more names were not found in your address book' ); * } * - * // Or just let it give you what it can - * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' ); - * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK" - * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US" + * // Or just let it give you what it can. Optionally fill in from a default. + * results = collection.get( wanted, 'nobody@example.com' ); + * mw.log( results['Jane Doe'] ); // "jane@example.org" + * mw.log( results['Daniel Jackson'] ); // "nobody@example.com" * * @class mw.Map * * @constructor - * @param {Object|boolean} [values] Value-bearing object to map, or boolean - * true to map over the global object. Defaults to an empty object. + * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an + * empty object. + * For backwards-compatibility with mw.config, this can also be `true` in which case values + * are copied to the Window object as global variables (T72470). Values are copied in + * one direction only. Changes to globals are not reflected in the map. */ function Map( values ) { - this.values = values === true ? window : ( values || {} ); - return this; + if ( values === true ) { + this.values = {}; + + // Override #set to also set the global variable + this.set = function ( selection, value ) { + var s; + + if ( $.isPlainObject( selection ) ) { + for ( s in selection ) { + setGlobalMapValue( this, s, selection[s] ); + } + return true; + } + if ( typeof selection === 'string' && arguments.length ) { + setGlobalMapValue( this, selection, value ); + return true; + } + return false; + }; + + return; + } + + this.values = values || {}; + } + + /** + * Alias property to the global object. + * + * @private + * @static + * @param {mw.Map} map + * @param {string} key + * @param {Mixed} value + */ + function setGlobalMapValue( map, key, value ) { + map.values[key] = value; + mw.log.deprecate( + window, + key, + value, + // Deprecation notice for mw.config globals (T58550, T72470) + map === mw.config && 'Use mw.config instead.' + ); } Map.prototype = { /** - * Get the value of one or multiple a keys. + * Get the value of one or more keys. * - * If called with no arguments, all values will be returned. + * If called with no arguments, all values are returned. * - * @param {string|Array} selection String key or array of keys to get values for. - * @param {Mixed} [fallback] Value to use in case key(s) do not exist. - * @return mixed If selection was a string returns the value or null, - * If selection was an array, returns an object of key/values (value is null if not found), - * If selection was not passed or invalid, will return the 'values' object member (be careful as - * objects are always passed by reference in JavaScript!). - * @return {string|Object|null} Values as a string or object, null if invalid/inexistant. + * @param {string|Array} [selection] Key or array of keys to retrieve values for. + * @param {Mixed} [fallback=null] Value for keys that don't exist. + * @return {Mixed|Object| null} If selection was a string, returns the value, + * If selection was an array, returns an object of key/values. + * If no selection is passed, the 'values' container is returned. (Beware that, + * as is the default in JavaScript, the object is returned by reference.) */ get: function ( selection, fallback ) { var results, i; @@ -127,16 +142,16 @@ return this.values; } - // invalid selection key + // Invalid selection key return null; }, /** - * Sets one or multiple key/value pairs. + * Set one or more key/value pairs. * - * @param {string|Object} selection String key to set value for, or object mapping keys to values. + * @param {string|Object} selection Key to set value for, or object mapping keys to values * @param {Mixed} [value] Value to set (optional, only in use when key is a string) - * @return {Boolean} This returns true on success, false on failure. + * @return {boolean} True on success, false on failure */ set: function ( selection, value ) { var s; @@ -155,10 +170,10 @@ }, /** - * Checks if one or multiple keys exist. + * Check if one or more keys exist. * - * @param {Mixed} selection String key or array of keys to check - * @return {boolean} Existence of key(s) + * @param {Mixed} selection Key or array of keys to check + * @return {boolean} True if the key(s) exist */ exists: function ( selection ) { var s; @@ -230,7 +245,7 @@ * @class mw.Message * * @constructor - * @param {mw.Map} map Message storage + * @param {mw.Map} map Message store * @param {string} key * @param {Array} [parameters] */ @@ -244,24 +259,22 @@ Message.prototype = { /** - * Simple message parser, does $N replacement and nothing else. + * Get parsed contents of the message. * + * The default parser does simple $N replacements and nothing else. * This may be overridden to provide a more complex message parser. - * - * The primary override is in mediawiki.jqueryMsg. + * The primary override is in the mediawiki.jqueryMsg module. * * This function will not be called for nonexistent messages. + * + * @return {string} Parsed message */ parser: function () { - var parameters = this.parameters; - return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) { - var index = parseInt( match, 10 ) - 1; - return parameters[index] !== undefined ? parameters[index] : '$' + match; - } ); + return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) ); }, /** - * Appends (does not replace) parameters for replacement to the .parameters property. + * Add (does not replace) parameters for `N$` placeholder values. * * @param {Array} parameters * @chainable @@ -275,9 +288,10 @@ }, /** - * Converts message object to its string form based on the state of format. + * Convert message object to its string form based on current format. * - * @return {string} Message as a string in the current form or `<key>` if key does not exist. + * @return {string} Message as a string in the current form, or `<key>` if key + * does not exist. */ toString: function () { var text; @@ -304,7 +318,7 @@ }, /** - * Changes format to 'parse' and converts message to string + * Change format to 'parse' and convert message to string * * If jqueryMsg is loaded, this parses the message text from wikitext * (where supported) to HTML @@ -319,7 +333,7 @@ }, /** - * Changes format to 'plain' and converts message to string + * Change format to 'plain' and convert message to string * * This substitutes parameters, but otherwise does not change the * message text. @@ -332,12 +346,14 @@ }, /** - * Changes format to 'text' and converts message to string + * Change format to 'text' and convert message to string * * If jqueryMsg is loaded, {{-transformation is done where supported * (such as {{plural:}}, {{gender:}}, {{int:}}). * - * Otherwise, it is equivalent to plain. + * Otherwise, it is equivalent to plain + * + * @return {string} String form of text message */ text: function () { this.format = 'text'; @@ -345,9 +361,9 @@ }, /** - * Changes the format to 'escaped' and converts message to string + * Change the format to 'escaped' and convert message to string * - * This is equivalent to using the 'text' format (see text method), then + * This is equivalent to using the 'text' format (see #text), then * HTML-escaping the output. * * @return {string} String form of html escaped message @@ -358,7 +374,7 @@ }, /** - * Checks if message exists + * Check if a message exists * * @see mw.Map#exists * @return {boolean} @@ -372,7 +388,6 @@ * @class mw */ mw = { - /* Public Members */ /** * Get the current time, measured in milliseconds since January 1, 1970 (UTC). @@ -392,6 +407,24 @@ }() ), /** + * Format a string. Replace $1, $2 ... $N with positional arguments. + * + * Used by Message#parser(). + * + * @since 1.25 + * @param {string} fmt Format string + * @param {Mixed...} parameters Values for $N replacements + * @return {string} Formatted string + */ + format: function ( formatString ) { + 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; + } ); + }, + + /** * Track an analytic event. * * This method provides a generic means for MediaWiki JavaScript code to capture state @@ -413,7 +446,7 @@ }, /** - * Register a handler for subset of analytic events, specified by topic + * Register a handler for subset of analytic events, specified by topic. * * Handlers will be called once for each tracked event, including any events that fired before the * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating @@ -423,6 +456,8 @@ * * @param {string} topic Handle events whose name starts with this string prefix * @param {Function} callback Handler to call for each matching tracked event + * @param {string} callback.topic + * @param {Object} [callback.data] */ trackSubscribe: function ( topic, callback ) { var seen = 0; @@ -438,14 +473,14 @@ } ); }, - // Make the Map constructor publicly available. + // Expose Map constructor Map: Map, - // Make the Message constructor publicly available. + // Expose Message constructor Message: Message, /** - * Map of configuration values + * Map of configuration values. * * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config) * on mediawiki.org. @@ -455,11 +490,14 @@ * * @property {mw.Map} config */ - // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule to an instance of `mw.Map`. + // Dummy placeholder later assigned in ResourceLoaderStartUpModule config: null, /** - * Empty object that plugins can be installed in. + * Empty object for third-party libraries, for cases where you don't + * want to add a new global, or the global is bad and needs containment + * or wrapping. + * * @property */ libs: {}, @@ -478,12 +516,18 @@ legacy: {}, /** - * Localization system + * Store for messages. + * * @property {mw.Map} */ messages: new Map(), - /* Public Methods */ + /** + * Store for templates associated with a module. + * + * @property {mw.Map} + */ + templates: new Map(), /** * Get a message object. @@ -492,11 +536,10 @@ * * @see mw.Message * @param {string} key Key of message to get - * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @param {Mixed...} parameters Values for $N replacements * @return {mw.Message} */ message: function ( key ) { - // Variadic arguments var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, @@ -508,7 +551,7 @@ * * @see mw.Message * @param {string} key Key of message to get - * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @param {Mixed...} parameters Values for $N replacements * @return {string} */ msg: function () { @@ -532,7 +575,7 @@ /** * Write a message the console's warning channel. * Also logs a stacktrace for easier debugging. - * Each action is silently ignored if the browser doesn't support it. + * Actions not supported by the browser console are silently ignored. * * @param {string...} msg Messages to output to console */ @@ -553,12 +596,14 @@ * @param {Object} obj Host object of deprecated property * @param {string} key Name of property to create in `obj` * @param {Mixed} val The value this property should return when accessed - * @param {string} [msg] Optional text to include in the deprecation message. + * @param {string} [msg] Optional text to include in the deprecation message */ log.deprecate = !Object.defineProperty ? function ( obj, key, val ) { obj[key] = val; } : function ( obj, key, val, msg ) { msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' ); + // Support: IE8 + // Can throw on Object.defineProperty. try { Object.defineProperty( obj, key, { configurable: true, @@ -575,7 +620,7 @@ } } ); } catch ( err ) { - // IE8 can throw on Object.defineProperty + // Fallback to creating a copy of the value to the object. obj[key] = val; } }; @@ -584,42 +629,74 @@ }() ), /** - * Client-side module loader which integrates with the MediaWiki ResourceLoader + * Client for ResourceLoader server end point. + * + * This client is in charge of maintaining the module registry and state + * machine, initiating network (batch) requests for loading modules, as + * well as dependency resolution and execution of source code. + * + * For more information, refer to + * <https://www.mediawiki.org/wiki/ResourceLoader/Features> + * * @class mw.loader * @singleton */ loader: ( function () { - /* Private Members */ + /** + * Fired via mw.track on various resource loading errors. + * + * @event resourceloader_exception + * @param {Error|Mixed} e The error that was thrown. Almost always an Error + * object, but in theory module code could manually throw something else, and that + * might also end up here. + * @param {string} [module] Name of the module which caused the error. Omitted if the + * error is not module-related or the module cannot be easily identified due to + * batched handling. + * @param {string} source Source of the error. Possible values: + * + * - style: stylesheet error (only affects old IE where a special style loading method + * is used) + * - load-callback: exception thrown by user callback + * - module-execute: exception thrown by module code + * - store-eval: could not evaluate module code cached in localStorage + * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init + * - store-localstorage-json: JSON conversion error in mw.loader.store.set + * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update + */ /** - * Mapping of registered modules + * Fired via mw.track on resource loading error conditions. + * + * @event resourceloader_assert + * @param {string} source Source of the error. Possible values: * - * The jquery module is pre-registered, because it must have already - * been provided for this object to have been built, and in debug mode - * jquery would have been provided through a unique loader request, - * making it impossible to hold back registration of jquery until after - * mediawiki. + * - bug-T59567: failed to cache script due to an Opera function -> string conversion + * bug; see <https://phabricator.wikimedia.org/T59567> for details + */ + + /** + * Mapping of registered modules. * - * For exact details on support for script, style and messages, look at - * mw.loader.implement. + * See #implement for exact details on support for script, style and messages. * * Format: + * * { * 'moduleName': { - * // At registry - * 'version': ############## (unix timestamp), + * // From startup mdoule + * 'version': ############## (unix timestamp) * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} - * 'group': 'somegroup', (or) null, - * 'source': 'local', 'someforeignwiki', (or) null - * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing' + * 'group': 'somegroup', (or) null + * 'source': 'local', (or) 'anotherwiki' * 'skip': 'return !!window.Example', (or) null + * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing' * * // Added during implementation - * 'skipped': true, - * 'script': ..., - * 'style': ..., - * 'messages': { 'key': 'value' }, + * 'skipped': true + * 'script': ... + * 'style': ... + * 'messages': { 'key': 'value' } * } * } * @@ -627,32 +704,37 @@ * @private */ var registry = {}, - // // Mapping of sources, keyed by source-id, values are strings. + // // Format: - // { - // 'sourceId': 'http://foo.bar/w/load.php' - // } + // + // { + // 'sourceId': 'http://example.org/w/load.php' + // } // sources = {}, + // List of modules which will be loaded as when ready batch = [], + // List of modules to be loaded queue = [], + // List of callback functions waiting for modules to be ready to be called jobs = [], + // Selector cache for the marker element. Use getMarker() to get/use the marker! $marker = null, - // Buffer for addEmbeddedCSS. + + // Buffer for #addEmbeddedCSS cssBuffer = '', - // Callbacks for addEmbeddedCSS. - cssCallbacks = $.Callbacks(); - /* Private methods */ + // Callbacks for #addEmbeddedCSS + cssCallbacks = $.Callbacks(); function getMarker() { - // Cached if ( !$marker ) { + // Cache $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); if ( !$marker.length ) { mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' ); @@ -663,60 +745,35 @@ } /** - * Create a new style tag and add it to the DOM. + * Create a new style element and add it to the DOM. * * @private * @param {string} text CSS text - * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be - * inserted before. Otherwise it will be appended to `<head>`. - * @return {HTMLElement} Reference to the created `<style>` element. + * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag + * should be inserted before + * @return {HTMLElement} Reference to the created style element */ function newStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); - // Insert into document before setting cssText (bug 33305) + // Support: IE + // Must attach to document before setting cssText (bug 33305) if ( nextnode ) { - // Must be inserted with native insertBefore, not $.fn.before. - // When using jQuery to insert it, like $nextnode.before( s ), - // then IE6 will throw "Access is denied" when trying to append - // to .cssText later. Some kind of weird security measure. - // http://stackoverflow.com/q/12586482/319266 - // Works: jsfiddle.net/zJzMy/1 - // Fails: jsfiddle.net/uJTQz - // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines) - if ( nextnode.jquery ) { - nextnode = nextnode.get( 0 ); - } - nextnode.parentNode.insertBefore( s, nextnode ); + $( nextnode ).before( s ); } else { document.getElementsByTagName( 'head' )[0].appendChild( s ); } if ( s.styleSheet ) { - // IE + // Support: IE6-10 + // Old IE ignores appended text nodes, access stylesheet directly. s.styleSheet.cssText = text; } else { - // Other browsers. - // (Safari sometimes borks on non-string values, - // play safe by casting to a string, just in case.) - s.appendChild( document.createTextNode( String( text ) ) ); + // Standard behaviour + s.appendChild( document.createTextNode( text ) ); } return s; } /** - * Checks whether it is safe to add this css to a stylesheet. - * - * @private - * @param {string} cssText - * @return {boolean} False if a new one must be created. - */ - function canExpandStylesheetWith( cssText ) { - // Makes sure that cssText containing `@import` - // rules will end up in a new stylesheet (as those only work when - // placed at the start of a stylesheet; bug 35562). - return cssText.indexOf( '@import' ) === -1; - } - - /** * Add a bit of CSS text to the current browser page. * * The CSS will be appended to an existing ResourceLoader-created `<style>` tag @@ -736,16 +793,18 @@ // Yield once before inserting the <style> tag. There are likely // more calls coming up which we can combine this way. // Appending a stylesheet and waiting for the browser to repaint - // is fairly expensive, this reduces it (bug 45810) + // is fairly expensive, this reduces that (bug 45810) if ( cssText ) { - // Be careful not to extend the buffer with css that needs a new stylesheet - if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) { + // Be careful not to extend the buffer with css that needs a new stylesheet. + // cssText containing `@import` rules needs to go at the start of a buffer, + // since those only work when placed at the start of a stylesheet; bug 35562. + if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) { // Linebreak for somewhat distinguishable sections // (the rl-cachekey comment separating each) cssBuffer += '\n' + cssText; // TODO: Use requestAnimationFrame in the future which will // perform even better by not injecting styles while the browser - // is paiting. + // is painting. setTimeout( function () { // Can't pass addEmbeddedCSS to setTimeout directly because Firefox // (below version 13) has the non-standard behaviour of passing a @@ -760,8 +819,9 @@ } else if ( cssBuffer ) { cssText = cssBuffer; cssBuffer = ''; + } else { - // This is a delayed call, but buffer is already cleared by + // This is a delayed call, but buffer was already cleared by // another delayed call. return; } @@ -774,21 +834,22 @@ if ( 'documentMode' in document && document.documentMode <= 9 ) { $style = getMarker().prev(); - // Verify that the the element before Marker actually is a + // Verify that the element before the marker actually is a // <style> tag and one that came from ResourceLoader // (not some other style tag or even a `<meta>` or `<script>`). if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) { // There's already a dynamic <style> tag present and - // canExpandStylesheetWith() gave a green light to append more to it. + // we are able to append more to it. styleEl = $style.get( 0 ); + // Support: IE6-10 if ( styleEl.styleSheet ) { try { - styleEl.styleSheet.cssText += cssText; // IE + styleEl.styleSheet.cssText += cssText; } catch ( e ) { - log( 'Stylesheet error', e ); + mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } ); } } else { - styleEl.appendChild( document.createTextNode( String( cssText ) ) ); + styleEl.appendChild( document.createTextNode( cssText ) ); } cssCallbacks.fire().empty(); return; @@ -801,39 +862,57 @@ } /** - * Generates an ISO8601 "basic" string from a UNIX timestamp + * Zero-pad three numbers. + * + * @private + * @param {number} a + * @param {number} b + * @param {number} c + * @return {string} + */ + function pad( a, b, c ) { + return ( + ( a < 10 ? '0' : '' ) + a + + ( b < 10 ? '0' : '' ) + b + + ( c < 10 ? '0' : '' ) + c + ); + } + + /** + * Convert UNIX timestamp to ISO8601 format. + * * @private + * @param {number} timestamp UNIX timestamp */ function formatVersionNumber( timestamp ) { var d = new Date(); - function pad( a, b, c ) { - return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); - } d.setTime( timestamp * 1000 ); return [ - pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T', - pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z' + pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), + 'T', + pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), + 'Z' ].join( '' ); } /** - * Resolves dependencies and detects circular references. + * Resolve dependencies and detect circular references. * * @private * @param {string} module Name of the top-level module whose dependencies shall be - * resolved and sorted. + * resolved and sorted. * @param {Array} resolved Returns a topological sort of the given module and its - * dependencies, such that later modules depend on earlier modules. The array - * contains the module names. If the array contains already some module names, - * this function appends its result to the pre-existing array. + * dependencies, such that later modules depend on earlier modules. The array + * contains the module names. If the array contains already some module names, + * this function appends its result to the pre-existing array. * @param {Object} [unresolved] Hash used to track the current dependency - * chain; used to report loops in the dependency graph. + * chain; used to report loops in the dependency graph. * @throws {Error} If any unregistered module or a dependency loop is encountered */ function sortDependencies( module, resolved, unresolved ) { var n, deps, len, skip; - if ( registry[module] === undefined ) { + if ( !hasOwn.call( registry, module ) ) { throw new Error( 'Unknown dependency: ' + module ); } @@ -859,10 +938,10 @@ } } if ( $.inArray( module, resolved ) !== -1 ) { - // Module already resolved; nothing to do. + // Module already resolved; nothing to do return; } - // unresolved is optional, supply it if not passed in + // Create unresolved if not passed in if ( !unresolved ) { unresolved = {}; } @@ -888,81 +967,37 @@ } /** - * Gets a list of module names that a module depends on in their proper dependency + * Get a list of module names that a module depends on in their proper dependency * order. * * @private - * @param {string} module Module name or array of string module names - * @return {Array} list of dependencies, including 'module'. - * @throws {Error} If circular reference is detected + * @param {string[]} module Array of string module names + * @return {Array} List of dependencies, including 'module'. */ - function resolve( module ) { - var m, resolved; - - // Allow calling with an array of module names - if ( $.isArray( module ) ) { - resolved = []; - for ( m = 0; m < module.length; m += 1 ) { - sortDependencies( module[m], resolved ); - } - return resolved; - } - - if ( typeof module === 'string' ) { - resolved = []; + function resolve( modules ) { + var resolved = []; + $.each( modules, function ( idx, module ) { sortDependencies( module, resolved ); - return resolved; - } - - throw new Error( 'Invalid module argument: ' + module ); + } ); + return resolved; } /** - * Narrows a list of module names down to those matching a specific - * state (see comment on top of this scope for a list of valid states). - * One can also filter for 'unregistered', which will return the - * modules names that don't have a registry entry. + * Determine whether all dependencies are in state 'ready', which means we may + * execute the module or job now. * * @private - * @param {string|string[]} states Module states to filter by - * @param {Array} [modules] List of module names to filter (optional, by default the entire - * registry is used) - * @return {Array} List of filtered module names + * @param {Array} module Names of modules to be checked + * @return {boolean} True if all modules are in state 'ready', false otherwise */ - function filter( states, modules ) { - var list, module, s, m; - - // Allow states to be given as a string - if ( typeof states === 'string' ) { - states = [states]; - } - // If called without a list of modules, build and use a list of all modules - list = []; - if ( modules === undefined ) { - modules = []; - for ( module in registry ) { - modules[modules.length] = module; - } - } - // Build a list of modules which are in one of the specified states - for ( s = 0; s < states.length; s += 1 ) { - for ( m = 0; m < modules.length; m += 1 ) { - if ( registry[modules[m]] === undefined ) { - // Module does not exist - if ( states[s] === 'unregistered' ) { - // OK, undefined - list[list.length] = modules[m]; - } - } else { - // Module exists, check state - if ( registry[modules[m]].state === states[s] ) { - // OK, correct state - list[list.length] = modules[m]; - } - } + function allReady( modules ) { + var i; + for ( i = 0; i < modules.length; i++ ) { + if ( mw.loader.getState( modules[i] ) !== 'ready' ) { + return false; } } - return list; + return true; } /** @@ -970,18 +1005,27 @@ * execute the module or job now. * * @private - * @param {Array} dependencies Dependencies (module names) to be checked. - * @return {boolean} True if all dependencies are in state 'ready', false otherwise + * @param {Array} modules Names of modules to be checked + * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise */ - function allReady( dependencies ) { - return filter( 'ready', dependencies ).length === dependencies.length; + function anyFailed( modules ) { + var i, state; + for ( i = 0; i < modules.length; i++ ) { + state = mw.loader.getState( modules[i] ); + if ( state === 'error' || state === 'missing' ) { + return true; + } + } + return false; } /** - * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs - * and modules that depend upon this module. if the given module failed, propagate the 'error' - * state up the dependency tree; otherwise, execute all jobs/modules that now have all their - * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any. + * A module has entered state 'ready', 'error', or 'missing'. Automatically update + * pending jobs and modules that depend upon this module. If the given module failed, + * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute + * all jobs/modules now having their dependencies satisfied. + * + * Jobs that depend on a failed module, will have their error callback ran (if any). * * @private * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'. @@ -989,16 +1033,15 @@ function handlePending( module ) { var j, job, hasErrors, m, stateChange; - // Modules. - if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) { + 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 ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) { - if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) { + if ( registry[m].state !== 'error' && registry[m].state !== 'missing' ) { + if ( anyFailed( registry[m].dependencies ) ) { registry[m].state = 'error'; stateChange = true; } @@ -1009,7 +1052,7 @@ // 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 = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0; + hasErrors = anyFailed( jobs[j].dependencies ); if ( hasErrors || allReady( jobs[j].dependencies ) ) { // All dependencies satisfied, or some have errors job = jobs[j]; @@ -1028,7 +1071,7 @@ } catch ( e ) { // A user-defined callback raised an exception. // Swallow it to protect our state machine! - log( 'Exception thrown by user callback', e ); + mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } ); } } } @@ -1091,7 +1134,7 @@ var key, value, media, i, urls, cssHandle, checkCssHandles, cssHandlesRegistered = false; - if ( registry[module] === undefined ) { + if ( !hasOwn.call( registry, module ) ) { throw new Error( 'Module has not been registered yet: ' + module ); } else if ( registry[module].state === 'registered' ) { throw new Error( 'Module has not been requested from the server yet: ' + module ); @@ -1108,7 +1151,8 @@ */ function addLink( media, url ) { var el = document.createElement( 'link' ); - // For IE: Insert in document *before* setting href + // Support: IE + // Insert in document *before* setting href getMarker().before( el ); el.rel = 'stylesheet'; if ( media && media !== 'all' ) { @@ -1153,8 +1197,8 @@ } 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 - log( 'Exception thrown by ' + module, e ); registry[module].state = 'error'; + mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } ); handlePending( module ); } } @@ -1170,6 +1214,11 @@ mw.messages.set( registry[module].messages ); } + // Initialise templates + if ( registry[module].templates ) { + mw.templates.set( module, registry[module].templates ); + } + if ( $.isReady || registry[module].async ) { // Make sure we don't run the scripts until all (potentially asynchronous) // stylesheet insertions have completed. @@ -1187,7 +1236,7 @@ var check = checkCssHandles; pending++; return function () { - if (check) { + if ( check ) { pending--; check(); check = undefined; // Revoke @@ -1271,8 +1320,6 @@ * Ignored (and defaulted to `true`) if the document-ready event has already occurred. */ function request( dependencies, ready, error, async ) { - var n; - // Allow calling by single module name if ( typeof dependencies === 'string' ) { dependencies = [dependencies]; @@ -1281,33 +1328,33 @@ // Add ready and error callbacks if they were given if ( ready !== undefined || error !== undefined ) { jobs[jobs.length] = { - 'dependencies': filter( - ['registered', 'loading', 'loaded'], - dependencies - ), - 'ready': ready, - 'error': error + dependencies: $.grep( dependencies, function ( module ) { + var state = mw.loader.getState( module ); + return state === 'registered' || state === 'loaded' || state === 'loading'; + } ), + ready: ready, + error: error }; } - // Queue up any dependencies that are registered - dependencies = filter( ['registered'], dependencies ); - for ( n = 0; n < dependencies.length; n += 1 ) { - if ( $.inArray( dependencies[n], queue ) === -1 ) { - queue[queue.length] = dependencies[n]; + $.each( dependencies, function ( idx, module ) { + var state = mw.loader.getState( module ); + if ( state === 'registered' && $.inArray( module, queue ) === -1 ) { + queue.push( module ); if ( async ) { - // Mark this module as async in the registry - registry[dependencies[n]].async = true; + registry[module].async = true; } } - } + } ); - // Work the queue mw.loader.work(); } function sortQuery( o ) { - var sorted = {}, key, a = []; + var key, + sorted = {}, + a = []; + for ( key in o ) { if ( hasOwn.call( o, key ) ) { a.push( key ); @@ -1326,7 +1373,9 @@ * @private */ function buildModulesString( moduleMap ) { - var arr = [], p, prefix; + var p, prefix, + arr = []; + for ( prefix in moduleMap ) { p = prefix === '' ? '' : prefix + '.'; arr.push( p + moduleMap[prefix].join( ',' ) ); @@ -1350,10 +1399,33 @@ currReqBase ); request = sortQuery( request ); - // Append &* to avoid triggering the IE6 extension check + // Support: IE6 + // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script + // isn't actually used in IE6, but MediaWiki enforces it in general. addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async ); } + /** + * Resolve indexed dependencies. + * + * ResourceLoader uses an optimization to save space which replaces module names in + * dependency lists with the index of that module within the array of module + * registration data if it exists. The benefit is a significant reduction in the data + * size of the startup module. This function changes those dependency lists back to + * arrays of strings. + * + * @param {Array} modules Modules array + */ + 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; + } ); + } + } ); + } + /* Public Members */ return { /** @@ -1389,12 +1461,12 @@ }; // Split module batch by source and by group. splits = {}; - maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 ); + maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 ); // 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' ) { + 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]; @@ -1433,7 +1505,7 @@ // repopulate these modules to the cache. // This means that at most one module will be useless (the one that had // the error) instead of all of them. - log( 'Error while evaluating data from mw.loader.store', err ); + mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } ); origBatch = $.grep( origBatch, function ( module ) { return registry[module].state === 'loading'; } ); @@ -1457,10 +1529,10 @@ for ( b = 0; b < batch.length; b += 1 ) { bSource = registry[batch[b]].source; bGroup = registry[batch[b]].group; - if ( splits[bSource] === undefined ) { + if ( !hasOwn.call( splits, bSource ) ) { splits[bSource] = {}; } - if ( splits[bSource][bGroup] === undefined ) { + if ( !hasOwn.call( splits[bSource], bGroup ) ) { splits[bSource][bGroup] = []; } bSourceGroup = splits[bSource][bGroup]; @@ -1513,7 +1585,7 @@ prefix = modules[i].substr( 0, lastDotIndex ); suffix = modules[i].slice( lastDotIndex + 1 ); - bytesAdded = moduleMap[prefix] !== undefined + bytesAdded = hasOwn.call( moduleMap, prefix ) ? suffix.length + 3 // '%2C'.length == 3 : modules[i].length + 3; // '%7C'.length == 3 @@ -1526,8 +1598,9 @@ moduleMap = {}; async = true; l = currReqBaseLength + 9; + mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } ); } - if ( moduleMap[prefix] === undefined ) { + if ( !hasOwn.call( moduleMap, prefix ) ) { moduleMap[prefix] = []; } moduleMap[prefix].push( suffix ); @@ -1569,7 +1642,7 @@ return true; } - if ( sources[id] !== undefined ) { + if ( hasOwn.call( sources, id ) ) { throw new Error( 'source already registered: ' + id ); } @@ -1586,7 +1659,12 @@ * Register a module, letting the system know about it and its * properties. Startup modules contain calls to this function. * - * @param {string} module Module name + * When using multiple module registration by passing an array, dependencies that + * are specified as references to modules within the array will be resolved before + * the modules are registered. + * + * @param {string|Array} module Module name or array of arrays, each containing + * a list of arguments compatible with this method * @param {number} version Module version number as a timestamp (falls backs to 0) * @param {string|Array|Function} dependencies One string or array of strings of module * names on which this module depends, or a function that returns that array. @@ -1595,16 +1673,17 @@ * @param {string} [skip=null] Script body of the skip function */ register: function ( module, version, dependencies, group, source, skip ) { - var m; + var i, len; // Allow multiple registration if ( typeof module === 'object' ) { - for ( m = 0; m < module.length; m += 1 ) { + resolveIndexedDependencies( module ); + for ( i = 0, len = module.length; i < len; i++ ) { // module is an array of module names - if ( typeof module[m] === 'string' ) { - mw.loader.register( module[m] ); + if ( typeof module[i] === 'string' ) { + mw.loader.register( module[i] ); // module is an array of arrays - } else if ( typeof module[m] === 'object' ) { - mw.loader.register.apply( mw.loader, module[m] ); + } else if ( typeof module[i] === 'object' ) { + mw.loader.register.apply( mw.loader, module[i] ); } } return; @@ -1613,7 +1692,7 @@ if ( typeof module !== 'string' ) { throw new Error( 'module must be a string, not a ' + typeof module ); } - if ( registry[module] !== undefined ) { + if ( hasOwn.call( registry, module ) ) { throw new Error( 'module already registered: ' + module ); } // List the module as registered @@ -1646,7 +1725,7 @@ * @param {string} module Name of module * @param {Function|Array} script Function with module code or Array of URLs to * be used as the src attribute of a new `<script>` tag. - * @param {Object} style Should follow one of the following patterns: + * @param {Object} [style] Should follow one of the following patterns: * * { "css": [css, ..] } * { "url": { <media>: [url, ..] } } @@ -1657,36 +1736,41 @@ * { <media>: [url, ..] } * * The reason css strings are not concatenated anymore is bug 31676. We now check - * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith). + * whether it's safe to extend the stylesheet. * - * @param {Object} msgs List of key/value pairs to be added to mw#messages. + * @param {Object} [msgs] List of key/value pairs to be added to mw#messages. + * @param {Object} [templates] List of key/value pairs to be added to mw#templates. */ - implement: function ( module, script, style, msgs ) { + implement: function ( module, script, style, msgs, templates ) { // Validate input if ( typeof module !== 'string' ) { - throw new Error( 'module must be a string, not a ' + typeof module ); + throw new Error( 'module must be of type string, not ' + typeof module ); } - if ( !$.isFunction( script ) && !$.isArray( script ) ) { - throw new Error( 'script must be a function or an array, not a ' + typeof script ); + if ( script && !$.isFunction( script ) && !$.isArray( script ) ) { + throw new Error( 'script must be of type function or array, not ' + typeof script ); } - if ( !$.isPlainObject( style ) ) { - throw new Error( 'style must be an object, not a ' + typeof style ); + if ( style && !$.isPlainObject( style ) ) { + throw new Error( 'style must be of type object, not ' + typeof style ); } - if ( !$.isPlainObject( msgs ) ) { - throw new Error( 'msgs must be an object, not a ' + typeof msgs ); + if ( msgs && !$.isPlainObject( msgs ) ) { + throw new Error( 'msgs must be of type object, not a ' + typeof msgs ); + } + if ( templates && !$.isPlainObject( templates ) ) { + throw new Error( 'templates must be of type object, not a ' + typeof templates ); } // Automatically register module - if ( registry[module] === undefined ) { + if ( !hasOwn.call( registry, module ) ) { mw.loader.register( module ); } // Check for duplicate implementation - if ( registry[module] !== undefined && 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 = msgs; + registry[module].script = script || []; + registry[module].style = style || {}; + registry[module].messages = msgs || {}; + 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'; @@ -1710,6 +1794,7 @@ * @param {Function} [ready] Callback to execute when all dependencies are ready * @param {Function} [error] Callback to execute if one or more dependencies failed * @return {jQuery.Promise} + * @since 1.23 this returns a promise */ using: function ( dependencies, ready, error ) { var deferred = $.Deferred(); @@ -1734,7 +1819,7 @@ if ( allReady( dependencies ) ) { // Run ready immediately deferred.resolve(); - } else if ( filter( ['error', 'missing'], dependencies ).length ) { + } else if ( anyFailed( dependencies ) ) { // Execute error immediately if any dependencies have errors deferred.reject( new Error( 'One or more dependencies failed to load' ), @@ -1761,7 +1846,7 @@ * Defaults to `true` if loading a URL, `false` otherwise. */ load: function ( modules, type, async ) { - var filtered, m, module, l; + var filtered, l; // Validate input if ( typeof modules !== 'object' && typeof modules !== 'string' ) { @@ -1769,16 +1854,16 @@ } // 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 ( async === undefined ) { // Assume async for bug 34542 async = true; } if ( type === 'text/css' ) { - // IE7-8 throws security warnings when inserting a <link> tag - // with a protocol-relative URL set though attributes (instead of - // properties) - when on HTTPS. See also bug 41331. + // Support: IE 7-8 + // Use properties instead of attributes as IE throws security + // warnings when inserting a <link> tag with a protocol-relative + // URL set though attributes - when on HTTPS. See bug 41331. l = document.createElement( 'link' ); l.rel = 'stylesheet'; l.href = modules; @@ -1801,26 +1886,18 @@ // Undefined modules are acceptable here in load(), because load() takes // an array of unrelated modules, whereas the modules passed to // using() are related and must all be loaded. - for ( filtered = [], m = 0; m < modules.length; m += 1 ) { - module = registry[modules[m]]; - if ( module !== undefined ) { - if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) { - filtered[filtered.length] = modules[m]; - } - } - } + filtered = $.grep( modules, function ( module ) { + var state = mw.loader.getState( module ); + return state !== null && state !== 'error' && state !== 'missing'; + } ); if ( filtered.length === 0 ) { return; } // Resolve entire dependency map filtered = resolve( filtered ); - // If all modules are ready, nothing to be done - if ( allReady( filtered ) ) { - return; - } - // If any modules have errors: also quit. - if ( filter( ['error', 'missing'], filtered ).length ) { + // If all modules are ready, or if any modules have errors, nothing to be done. + if ( allReady( filtered ) || anyFailed( filtered ) ) { return; } // Since some modules are not yet ready, queue up a request. @@ -1842,7 +1919,7 @@ } return; } - if ( registry[module] === undefined ) { + if ( !hasOwn.call( registry, module ) ) { mw.loader.register( module ); } if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1 @@ -1859,27 +1936,29 @@ /** * Get the version of a module. * - * @param {string} module Name of module to get version for + * @param {string} module Name of module * @return {string|null} The version, or null if the module (or its version) is not * in the registry. */ getVersion: function ( module ) { - if ( registry[module] !== undefined && registry[module].version !== undefined ) { - return formatVersionNumber( registry[module].version ); + if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) { + return null; } - return null; + return formatVersionNumber( registry[module].version ); }, /** * Get the state of a module. * - * @param {string} module Name of module to get state for + * @param {string} module Name of module + * @return {string|null} The state, or null if the module (or its state) is not + * in the registry. */ getState: function ( module ) { - if ( registry[module] !== undefined && registry[module].state !== undefined ) { - return registry[module].state; + if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) { + return null; } - return null; + return registry[module].state; }, /** @@ -1944,7 +2023,7 @@ }, /** - * Get a string key on which to vary the module cache. + * Get a key on which to vary the module cache. * @return {string} String of concatenated vary conditions. */ getVary: function () { @@ -1956,13 +2035,13 @@ }, /** - * Get a string key for a specific module. The key format is '[name]@[version]'. + * Get a key for a specific module. The key format is '[name]@[version]'. * * @param {string} module Module name * @return {string|null} Module key or null if module does not exist */ getModuleKey: function ( module ) { - return typeof registry[module] === 'object' ? + return hasOwn.call( registry, module ) ? ( module + '@' + registry[module].version ) : null; }, @@ -1985,8 +2064,15 @@ return; } - if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) { - // Disabled by configuration, or because debug mode is set + if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) { + // Disabled by configuration. + // Clear any previous store to free up space. (T66721) + mw.loader.store.clear(); + mw.loader.store.enabled = false; + return; + } + if ( mw.config.get( 'debug' ) ) { + // Disable module store in debug mode mw.loader.store.enabled = false; return; } @@ -2001,7 +2087,7 @@ return; } } catch ( e ) { - log( 'Storage error', e ); + mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } ); } if ( raw === undefined ) { @@ -2057,7 +2143,8 @@ // Unversioned, private, or site-/user-specific ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) || // Partial descriptor - $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1 + $.inArray( undefined, [ descriptor.script, descriptor.style, + descriptor.messages, descriptor.templates ] ) !== -1 ) { // Decline to store return false; @@ -2070,16 +2157,17 @@ String( descriptor.script ) : JSON.stringify( descriptor.script ), JSON.stringify( descriptor.style ), - JSON.stringify( descriptor.messages ) + JSON.stringify( descriptor.messages ), + JSON.stringify( descriptor.templates ) ]; - // Attempted workaround for a possible Opera bug (bug 57567). + // 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]; - log( 'Detected malformed function stringification (bug 57567)' ); + mw.track( 'resourceloader.assert', { source: 'bug-T59567' } ); } } catch ( e ) { - log( 'Storage error', e ); + mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } ); return; } @@ -2151,7 +2239,7 @@ data = JSON.stringify( mw.loader.store ); localStorage.setItem( key, data ); } catch ( e ) { - log( 'Storage error', e ); + mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } ); } } @@ -2223,8 +2311,8 @@ * - null or undefined: The short closing form is used, e.g. `<br/>`. * - this.Raw: The value attribute is included without escaping. * - this.Cdata: The value attribute is included, and an exception is - * thrown if it contains an illegal ETAGO delimiter. - * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>. + * thrown if it contains an illegal ETAGO delimiter. + * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>. * @return {string} HTML */ element: function ( name, attrs, contents ) { @@ -2387,13 +2475,49 @@ // @deprecated since 1.23 Use $ or jQuery instead mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' ); - // Attach to window and globally alias - window.mw = window.mediaWiki = mw; + /** + * Log a message to window.console, if possible. + * + * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs + * also in production mode). Gets console references in each invocation instead of caching the + * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE). + * + * @private + * @method log_ + * @param {string} topic Stream name passed by mw.track + * @param {Object} data Data passed by mw.track + * @param {Error} [data.exception] + * @param {string} data.source Error source + * @param {string} [data.module] Name of module which caused the error + */ + function log( topic, data ) { + var msg, + e = data.exception, + source = data.source, + module = data.module, + console = window.console; - // Auto-register from pre-loaded startup scripts - if ( $.isFunction( window.startUp ) ) { - window.startUp(); - window.startUp = undefined; + if ( console && console.log ) { + msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source; + if ( module ) { + msg += ' in module ' + module; + } + msg += ( e ? ':' : '.' ); + console.log( msg ); + + // If we have an exception object, log it to the error channel to trigger a + // proper stacktraces in browsers that support it. No fallback as we have no browsers + // that don't support error(), but do support log(). + if ( e && console.error ) { + console.error( String( e ), e ); + } + } } + // subscribe to error streams + mw.trackSubscribe( 'resourceloader.exception', log ); + mw.trackSubscribe( 'resourceloader.assert', log ); + + // Attach to window and globally alias + window.mw = window.mediaWiki = mw; }( jQuery ) ); |