/** * Base library for MediaWiki. * * Exposed globally as `mediaWiki` with `mw` as shortcut. * * @class mw * @alternateClassName mediaWiki * @singleton */ /*jshint latedef:false */ /*global sha1 */ ( function ( $ ) { 'use strict'; var mw, hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice, trackCallbacks = $.Callbacks( 'memory' ), trackHandlers = [], trackQueue = []; /** * 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 collection, query, results; * * // Create your address book * collection = new mw.Map(); * * // This data could be coming from an external source (eg. API/AJAX) * collection.set( { * 'John Doe': 'john@example.org', * 'Jane Doe': 'jane@example.org', * 'George van Halen': 'gvanhalen@example.org' * } ); * * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson']; * * // You can detect missing keys first * 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. 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] 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 ) { 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 more keys. * * If called with no arguments, all values are returned. * * @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; // If we only do this in the `return` block, it'll fail for the // call to get() from the mutli-selection block. fallback = arguments.length > 1 ? fallback : null; if ( $.isArray( selection ) ) { selection = slice.call( selection ); results = {}; for ( i = 0; i < selection.length; i++ ) { results[ selection[ i ] ] = this.get( selection[ i ], fallback ); } return results; } if ( typeof selection === 'string' ) { if ( !hasOwn.call( this.values, selection ) ) { return fallback; } return this.values[ selection ]; } if ( selection === undefined ) { return this.values; } // Invalid selection key return null; }, /** * Set one or more key/value pairs. * * @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} True on success, false on failure */ set: function ( selection, value ) { var s; if ( $.isPlainObject( selection ) ) { for ( s in selection ) { this.values[ s ] = selection[ s ]; } return true; } if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[ selection ] = value; return true; } return false; }, /** * Check if one or more keys exist. * * @param {Mixed} selection Key or array of keys to check * @return {boolean} True if the key(s) exist */ exists: function ( selection ) { var s; if ( $.isArray( selection ) ) { for ( s = 0; s < selection.length; s++ ) { if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) { return false; } } return true; } return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** * Object constructor for messages. * * Similar to the Message class in MediaWiki PHP. * * Format defaults to 'text'. * * @example * * var obj, str; * mw.messages.set( { * 'hello': 'Hello world', * 'hello-user': 'Hello, $1!', * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3' * } ); * * obj = new mw.Message( mw.messages, 'hello' ); * mw.log( obj.text() ); * // Hello world * * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] ); * mw.log( obj.text() ); * // Hello, John Doe! * * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] ); * mw.log( obj.text() ); * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago * * // Using mw.message shortcut * obj = mw.message( 'hello-user', 'John Doe' ); * mw.log( obj.text() ); * // Hello, John Doe! * * // Using mw.msg shortcut * str = mw.msg( 'hello-user', 'John Doe' ); * mw.log( str ); * // Hello, John Doe! * * // Different formats * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] ); * * obj.format = 'text'; * str = obj.toString(); * // Same as: * str = obj.text(); * * mw.log( str ); * // Hello, John "Wiki" <3 Doe! * * mw.log( obj.escaped() ); * // Hello, John "Wiki" <3 Doe! * * @class mw.Message * * @constructor * @param {mw.Map} map Message store * @param {string} key * @param {Array} [parameters] */ function Message( map, key, parameters ) { this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); return this; } Message.prototype = { /** * 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 the mediawiki.jqueryMsg module. * * This function will not be called for nonexistent messages. * * @return {string} Parsed message */ parser: function () { return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) ); }, /** * Add (does not replace) parameters for `N$` placeholder values. * * @param {Array} parameters * @chainable */ params: function ( parameters ) { var i; for ( i = 0; i < parameters.length; i += 1 ) { this.parameters.push( parameters[ i ] ); } return this; }, /** * Convert message object to its string form based on current format. * * @return {string} Message as a string in the current form, or `` if key * does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use as text if key does not exist if ( this.format === 'escaped' || this.format === 'parse' ) { // format 'escaped' and 'parse' need to have the brackets and key html escaped return mw.html.escape( '<' + this.key + '>' ); } return '<' + this.key + '>'; } if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } if ( this.format === 'escaped' ) { text = this.parser(); text = mw.html.escape( text ); } return text; }, /** * Change format to 'parse' and convert message to string * * If jqueryMsg is loaded, this parses the message text from wikitext * (where supported) to HTML * * Otherwise, it is equivalent to plain. * * @return {string} String form of parsed message */ parse: function () { this.format = 'parse'; return this.toString(); }, /** * Change format to 'plain' and convert message to string * * This substitutes parameters, but otherwise does not change the * message text. * * @return {string} String form of plain message */ plain: function () { this.format = 'plain'; return this.toString(); }, /** * 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 * * @return {string} String form of text message */ text: function () { this.format = 'text'; return this.toString(); }, /** * Change the format to 'escaped' and convert message to string * * This is equivalent to using the 'text' format (see #text), then * HTML-escaping the output. * * @return {string} String form of html escaped message */ escaped: function () { this.format = 'escaped'; return this.toString(); }, /** * Check if a message exists * * @see mw.Map#exists * @return {boolean} */ exists: function () { return this.map.exists( this.key ); } }; /** * @class mw */ mw = { /** * Get the current time, measured in milliseconds since January 1, 1970 (UTC). * * On browsers that implement the Navigation Timing API, this function will produce floating-point * values with microsecond precision that are guaranteed to be monotonic. On all other browsers, * it will fall back to using `Date`. * * @return {number} Current time */ now: ( function () { var perf = window.performance, navStart = perf && perf.timing && perf.timing.navigationStart; return navStart && typeof perf.now === 'function' ? function () { return navStart + perf.now(); } : function () { return +new Date(); }; }() ), /** * 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 * information for analysis. Each logged event specifies a string topic name that describes * the kind of event that it is. Topic names consist of dot-separated path components, * arranged from most general to most specific. Each path component should have a clear and * well-defined purpose. * * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of * events that match their subcription, including those that fired before the handler was * bound. * * @param {string} topic Topic name * @param {Object} [data] Data describing the event, encoded as an object */ track: function ( topic, data ) { trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } ); trackCallbacks.fire( trackQueue ); }, /** * 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 * the exact time at which the event fired, a string 'topic' property naming the event, and a * 'data' property which is an object of event-specific data. The event topic and event data are * also passed to the callback as the first and second arguments, respectively. * * @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; function handler( trackQueue ) { var event; for ( ; seen < trackQueue.length; seen++ ) { event = trackQueue[ seen ]; if ( event.topic.indexOf( topic ) === 0 ) { callback.call( event, event.topic, event.data ); } } } trackHandlers.push( [ handler, callback ] ); trackCallbacks.add( handler ); }, /** * Stop handling events for a particular handler * * @param {Function} callback */ trackUnsubscribe: function ( callback ) { trackHandlers = $.grep( trackHandlers, function ( fns ) { if ( fns[ 1 ] === callback ) { trackCallbacks.remove( fns[ 0 ] ); // Ensure the tuple is removed to avoid holding on to closures return false; } return true; } ); }, // Expose Map constructor Map: Map, // Expose Message constructor Message: Message, /** * 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. * * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the * global `window` object. * * @property {mw.Map} config */ // Dummy placeholder later assigned in ResourceLoaderStartUpModule config: null, /** * 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: {}, /** * Access container for deprecated functionality that can be moved from * from their legacy location and attached to this object (e.g. a global * function that is deprecated and as stop-gap can be exposed through here). * * This was reserved for future use but never ended up being used. * * @deprecated since 1.22 Let deprecated identifiers keep their original name * and use mw.log#deprecate to create an access container for tracking. * @property */ legacy: {}, /** * Store for messages. * * @property {mw.Map} */ messages: new Map(), /** * Store for templates associated with a module. * * @property {mw.Map} */ templates: new Map(), /** * Get a message object. * * Shorcut for `new mw.Message( mw.messages, key, parameters )`. * * @see mw.Message * @param {string} key Key of message to get * @param {Mixed...} parameters Values for $N replacements * @return {mw.Message} */ message: function ( key ) { var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** * Get a message string using the (default) 'text' format. * * Shortcut for `mw.message( key, parameters... ).text()`. * * @see mw.Message * @param {string} key Key of message to get * @param {Mixed...} parameters Values for $N replacements * @return {string} */ msg: function () { return mw.message.apply( mw.message, arguments ).toString(); }, /** * Dummy placeholder for {@link mw.log} * * @method */ log: ( function () { // Also update the restoration of methods in mediawiki.log.js // when adding or removing methods here. var log = function () {}; /** * @class mw.log * @singleton */ /** * Write a message the console's warning channel. * Actions not supported by the browser console are silently ignored. * * @param {string...} msg Messages to output to console */ log.warn = function () { var console = window.console; if ( console && console.warn && console.warn.apply ) { console.warn.apply( console, arguments ); } }; /** * Write a message the console's error channel. * * Most browsers provide a stacktrace by default if the argument * is a caught Error object. * * @since 1.26 * @param {Error|string...} msg Messages to output to console */ log.error = function () { var console = window.console; if ( console && console.error && console.error.apply ) { console.error.apply( console, arguments ); } }; /** * Create a property in a host object that, when accessed, will produce * a deprecation warning in the console with backtrace. * * @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 */ 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, enumerable: true, get: function () { mw.track( 'mw.deprecate', key ); mw.log.warn( msg ); return val; }, set: function ( newVal ) { mw.track( 'mw.deprecate', key ); mw.log.warn( msg ); val = newVal; } } ); } catch ( err ) { // Fallback to creating a copy of the value to the object. obj[ key ] = val; } }; return log; }() ), /** * 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 * * * @class mw.loader * @singleton */ loader: ( function () { /** * 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 */ /** * Fired via mw.track on resource loading error conditions. * * @event resourceloader_assert * @param {string} source Source of the error. Possible values: * * - bug-T59567: failed to cache script due to an Opera function -> string conversion * bug; see for details */ /** * Mapping of registered modules. * * See #implement and #execute for exact details on support for script, style and messages. * * Format: * * { * 'moduleName': { * // From mw.loader.register() * 'version': '########' (hash) * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} * 'group': 'somegroup', (or) null * 'source': 'local', (or) 'anotherwiki' * 'skip': 'return !!window.Example', (or) null * * // Set from execute() or mw.loader.state() * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing' * * // Optionally added at run-time by mw.loader.implement() * 'skipped': true * 'script': closure, array of urls, or string * 'style': { ... } (see #execute) * 'messages': { 'key': 'value', ... } * } * } * * 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 */ var registry = {}, // Mapping of sources, keyed by source-id, values are strings. // // Format: // // { // '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 jobs waiting for modules to be ready. * * Jobs are created by #request() and run by #handlePending(). * * Typically when a job is created for a module, the job's dependencies contain * both the module being requested and all its recursive dependencies. * * Format: * * { * 'dependencies': [ module names ], * 'ready': Function callback * 'error': Function callback * } * * @property {Object[]} jobs * @private */ jobs = [], // Selector cache for the marker element. Use getMarker() to get/use the marker! $marker = null, // Buffer for #addEmbeddedCSS cssBuffer = '', // Callbacks for #addEmbeddedCSS cssCallbacks = $.Callbacks(); function getMarker() { if ( !$marker ) { // Cache $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); if ( !$marker.length ) { mw.log( 'No found, inserting dynamically' ); $marker = $( '' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); } } return $marker; } /** * 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 * @return {HTMLElement} Reference to the created style element */ function newStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); // Support: IE // Must attach to document before setting cssText (bug 33305) if ( nextnode ) { $( nextnode ).before( s ); } else { document.getElementsByTagName( 'head' )[ 0 ].appendChild( s ); } if ( s.styleSheet ) { // Support: IE6-10 // Old IE ignores appended text nodes, access stylesheet directly. s.styleSheet.cssText = text; } else { // Standard behaviour s.appendChild( document.createTextNode( text ) ); } return s; } /** * Add a bit of CSS text to the current browser page. * * The CSS will be appended to an existing ResourceLoader-created `