X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/resources/src/mediawiki/mediawiki.js diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js new file mode 100644 index 00000000..b5224867 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.js @@ -0,0 +1,2799 @@ +/** + * Base library for MediaWiki. + * + * Exposed globally as `mediaWiki` with `mw` as shortcut. + * + * @class mw + * @alternateClassName mediaWiki + * @singleton + */ + +/* global mwNow */ +/* eslint-disable no-use-before-define */ + +( function ( $ ) { + 'use strict'; + + var mw, StringSet, log, + hasOwn = Object.prototype.hasOwnProperty, + slice = Array.prototype.slice, + trackCallbacks = $.Callbacks( 'memory' ), + trackHandlers = [], + trackQueue = []; + + /** + * FNV132 hash function + * + * This function implements the 32-bit version of FNV-1. + * It is equivalent to hash( 'fnv132', ... ) in PHP, except + * its output is base 36 rather than hex. + * See + * + * @private + * @param {string} str String to hash + * @return {string} hash as an seven-character base 36 string + */ + function fnv132( str ) { + /* eslint-disable no-bitwise */ + var hash = 0x811C9DC5, + i; + + for ( i = 0; i < str.length; i++ ) { + hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 ); + hash ^= str.charCodeAt( i ); + } + + hash = ( hash >>> 0 ).toString( 36 ); + while ( hash.length < 7 ) { + hash = '0' + hash; + } + + return hash; + /* eslint-enable no-bitwise */ + } + + function defineFallbacks() { + // + StringSet = window.Set || ( function () { + /** + * @private + * @class + */ + function StringSet() { + this.set = {}; + } + StringSet.prototype.add = function ( value ) { + this.set[ value ] = true; + }; + StringSet.prototype.has = function ( value ) { + return hasOwn.call( this.set, value ); + }; + return StringSet; + }() ); + } + + /** + * Create an object that can be read from or written to via methods that allow + * interaction both with single and multiple properties at once. + * + * @private + * @class mw.Map + * + * @constructor + * @param {boolean} [global=false] Whether to synchronise =values to the global + * window object (for backwards-compatibility with mw.config; T72470). Values are + * copied in one direction only. Changes to globals do not reflect in the map. + */ + function Map( global ) { + this.values = {}; + if ( global === true ) { + // 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; + }; + } + } + + /** + * 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; + log.deprecate( + window, + key, + value, + // Deprecation notice for mw.config globals (T58550, T72470) + map === mw.config && 'Use mw.config instead.' + ); + } + + Map.prototype = { + constructor: Map, + + /** + * 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, a new object with all key/values is returned. + */ + get: function ( selection, fallback ) { + var results, i; + fallback = arguments.length > 1 ? fallback : null; + + if ( Array.isArray( selection ) ) { + results = {}; + for ( i = 0; i < selection.length; i++ ) { + if ( typeof selection[ i ] === 'string' ) { + results[ selection[ i ] ] = hasOwn.call( this.values, selection[ i ] ) ? + this.values[ selection[ i ] ] : + fallback; + } + } + return results; + } + + if ( typeof selection === 'string' ) { + return hasOwn.call( this.values, selection ) ? + this.values[ selection ] : + fallback; + } + + if ( selection === undefined ) { + results = {}; + for ( i in this.values ) { + results[ i ] = this.values[ i ]; + } + return results; + } + + // Invalid selection key + return fallback; + }, + + /** + * 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 i; + if ( Array.isArray( selection ) ) { + for ( i = 0; i < selection.length; i++ ) { + if ( typeof selection[ i ] !== 'string' || !hasOwn.call( this.values, selection[ i ] ) ) { + 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 ) ); + }, + + // eslint-disable-next-line valid-jsdoc + /** + * 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++ ) { + 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 ⧼key⧽ as text if key does not exist + // Err on the side of safety, ensure that the output + // is always html safe in the event the message key is + // missing, since in that case its highly likely the + // message key is user-controlled. + // '⧼' is used instead of '<' to side-step any + // double-escaping issues. + // (Keep synchronised with Message::toString() in PHP.) + return '⧼' + mw.html.escape( 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 ); + } + }; + + defineFallbacks(); + + /* eslint-disable no-console */ + log = ( function () { + /** + * Write a verbose message to the browser's console in debug mode. + * + * This method is mainly intended for verbose logging. It is a no-op in production mode. + * In ResourceLoader debug mode, it will use the browser's console if available, with + * fallback to creating a console interface in the DOM and logging messages there. + * + * See {@link mw.log} for other logging methods. + * + * @member mw + * @param {...string} msg Messages to output to console. + */ + var log = function () {}, + console = window.console; + + // Note: Keep list of methods in sync with restoration in mediawiki.log.js + // when adding or removing mw.log methods below! + + /** + * Collection of methods to help log messages to the console. + * + * @class mw.log + * @singleton + */ + + /** + * Write a message to the browser console's warning channel. + * + * This method is a no-op in browsers that don't implement the Console API. + * + * @param {...string} msg Messages to output to console + */ + log.warn = console && console.warn && Function.prototype.bind ? + Function.prototype.bind.call( console.warn, console ) : + $.noop; + + /** + * Write a message to the browser console's error channel. + * + * Most browsers also print a stacktrace when calling this method if the + * argument is an Error object. + * + * This method is a no-op in browsers that don't implement the Console API. + * + * @since 1.26 + * @param {Error|...string} msg Messages to output to console + */ + log.error = console && console.error && Function.prototype.bind ? + Function.prototype.bind.call( console.error, console ) : + $.noop; + + /** + * Create a property on a host object that, when accessed, will produce + * a deprecation warning in the console. + * + * @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} [logName=key] Optional custom name for the feature. + * This is used instead of `key` in the message and `mw.deprecate` tracking. + */ + log.deprecate = !Object.defineProperty ? function ( obj, key, val ) { + obj[ key ] = val; + } : function ( obj, key, val, msg, logName ) { + var logged = new StringSet(); + logName = logName || key; + msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' ); + function uniqueTrace() { + var trace = new Error().stack; + if ( logged.has( trace ) ) { + return false; + } + logged.add( trace ); + return true; + } + // Support: Safari 5.0 + // Throws "not supported on DOM Objects" for Node or Element objects (incl. document) + // Safari 4.0 doesn't have this method, and it was fixed in Safari 5.1. + try { + Object.defineProperty( obj, key, { + configurable: true, + enumerable: true, + get: function () { + if ( uniqueTrace() ) { + mw.track( 'mw.deprecate', logName ); + mw.log.warn( msg ); + } + return val; + }, + set: function ( newVal ) { + if ( uniqueTrace() ) { + mw.track( 'mw.deprecate', logName ); + mw.log.warn( msg ); + } + val = newVal; + } + } ); + } catch ( err ) { + obj[ key ] = val; + } + }; + + return log; + }() ); + /* eslint-enable no-console */ + + /** + * @class mw + */ + mw = { + redefineFallbacksForTest: function () { + if ( !window.QUnit ) { + throw new Error( 'Reset not allowed outside unit tests' ); + } + defineFallbacks(); + }, + + /** + * 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: mwNow, + // mwNow is defined in startup.js + + /** + * Format a string. Replace $1, $2 ... $N with positional arguments. + * + * Used by Message#parser(). + * + * @since 1.25 + * @param {string} formatString 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. + * + * Shortcut 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(); + }, + + // Expose mw.log + log: 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 + * - resolve: failed to sort dependencies for a module in mw.loader.load + * - 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 + * 'module': export Object + * + * // 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 required. + * Meta data is registered via mw.loader#register. Calls to that method are + * generated server-side by the startup module. + * - `loading`: + * The module was required through mw.loader (either directly or as dependency of + * another module). The client will fetch module contents from the server. + * The contents are then stashed in the registry via mw.loader#implement. + * - `loaded`: + * The module has been loaded from the server and stashed via mw.loader#implement. + * If the module has no more dependencies in-flight, the module will be executed + * immediately. 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 = {}, + + // For queueModuleScript() + handlingPendingRequests = false, + pendingRequests = [], + + // List of modules to be loaded + queue = [], + + /** + * List of callback jobs waiting for modules to be ready. + * + * Jobs are created by #enqueue() and run by #handlePending(). + * + * Typically when a job is created for a module, the job's dependencies contain + * both the required module and all its recursive dependencies. + * + * Format: + * + * { + * 'dependencies': [ module names ], + * 'ready': Function callback + * 'error': Function callback + * } + * + * @property {Object[]} jobs + * @private + */ + jobs = [], + + // For getMarker() + marker = null, + + // For addEmbeddedCSS() + cssBuffer = '', + cssBufferTimer = null, + cssCallbacks = $.Callbacks(), + rAF = window.requestAnimationFrame || setTimeout; + + function getMarker() { + if ( !marker ) { + // Cache + marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ); + if ( !marker ) { + mw.log( 'Create dynamically' ); + marker = $( '' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' )[ 0 ]; + } + } + return marker; + } + + /** + * Create a new style element and add it to the DOM. + * + * @private + * @param {string} text CSS text + * @param {Node} [nextNode] 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' ); + + s.appendChild( document.createTextNode( text ) ); + if ( nextNode && nextNode.parentNode ) { + nextNode.parentNode.insertBefore( s, nextNode ); + } else { + document.getElementsByTagName( 'head' )[ 0 ].appendChild( s ); + } + + return s; + } + + /** + * Add a bit of CSS text to the current browser page. + * + * The CSS will be appended to an existing ResourceLoader-created `