From: Aaron Schulz Date: Wed, 20 Jun 2018 15:14:28 +0000 (+0100) Subject: resourceloader: spin base module code out as a proper module X-Git-Tag: 1.34.0-rc.0~4972^2 X-Git-Url: http://git.cyclocoop.org/%22.htmlspecialchars%28%24url_syndic%29.%22?a=commitdiff_plain;h=b87c6d3f0b428185d532b50aa1c5494030c365ae;p=lhc%2Fweb%2Fwiklou.git resourceloader: spin base module code out as a proper module Bug: T192623 Change-Id: I6f7dc40488a990d0f8a25e84ebc9eb25ad4c2975 --- diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 0416c8550d..4777545d23 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -336,7 +336,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { * @return array */ public static function getStartupModules() { - return [ 'jquery', 'mediawiki' ]; + return [ 'jquery', 'mediawiki', 'mediawiki.base' ]; } public static function getLegacyModules() { diff --git a/maintenance/jsduck/eg-iframe.html b/maintenance/jsduck/eg-iframe.html index f19a69b34b..8475dcaf4e 100644 --- a/maintenance/jsduck/eg-iframe.html +++ b/maintenance/jsduck/eg-iframe.html @@ -43,8 +43,8 @@ - - + + diff --git a/resources/Resources.php b/resources/Resources.php index b7ecd10c61..258434e8e1 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -848,12 +848,18 @@ return [ 'scripts' => [ 'resources/src/mediawiki/mediawiki.js', 'resources/src/mediawiki/mediawiki.requestIdleCallback.js', - 'resources/src/mediawiki/mediawiki.errorLogger.js', - 'resources/src/mediawiki/mediawiki.base.js', ], 'debugScripts' => 'resources/src/mediawiki/mediawiki.log.js', 'targets' => [ 'desktop', 'mobile' ], ], + 'mediawiki.base' => [ + // Keep in sync with maintenance/jsduck/eg-iframe.html + 'scripts' => [ + 'resources/src/mediawiki.base/mediawiki.errorLogger.js', + 'resources/src/mediawiki.base/mediawiki.base.js', + ], + 'targets' => [ 'desktop', 'mobile' ], + ], 'mediawiki.apihelp' => [ 'styles' => 'resources/src/mediawiki.apihelp.css', 'targets' => [ 'desktop' ], diff --git a/resources/src/mediawiki.base/mediawiki.base.js b/resources/src/mediawiki.base/mediawiki.base.js new file mode 100644 index 0000000000..ed24af3065 --- /dev/null +++ b/resources/src/mediawiki.base/mediawiki.base.js @@ -0,0 +1,644 @@ +/*! + * This file is currently loaded as part of the 'mediawiki' module and therefore + * concatenated to mediawiki.js and executed at the same time. This file exists + * to help prepare for splitting up the 'mediawiki' module. + * This effort is tracked at https://phabricator.wikimedia.org/T192623 + * + * In short: + * + * - mediawiki.js will be reduced to the minimum needed to define mw.loader and + * mw.config, and then moved to its own private "mediawiki.loader" module that + * can be embedded within the StartupModule response. + * + * - mediawiki.base.js and other files in this directory will remain part of the + * "mediawiki" module, and will remain a default/implicit dependency for all + * regular modules, just like jquery and wikibits already are. + */ +/* globals mw */ +( function () { + 'use strict'; + + var slice = Array.prototype.slice, + mwLoaderTrack = mw.track, + trackCallbacks = $.Callbacks( 'memory' ), + trackHandlers = [], + hasOwn = Object.prototype.hasOwnProperty; + + /** + * 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 + * @return {mw.Message} + * @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 ); + } + }; + + /** + * @class mw + * @singleton + */ + + /** + * @inheritdoc mw.inspect#runReports + * @method + */ + mw.inspect = function () { + var args = arguments; + mw.loader.using( 'mediawiki.inspect', function () { + mw.inspect.runReports.apply( mw.inspect, args ); + } ); + }; + + /** + * 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 + */ + mw.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; + } ); + }; + + // Expose Message constructor + mw.Message = Message; + + /** + * 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} + */ + mw.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} + */ + mw.msg = function () { + return mw.message.apply( mw.message, arguments ).toString(); + }; + + /** + * 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 + */ + mw.track = function ( topic, data ) { + mwLoaderTrack( topic, data ); + trackCallbacks.fire( mw.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] + */ + mw.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 + */ + mw.trackUnsubscribe = function ( callback ) { + trackHandlers = trackHandlers.filter( 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; + } ); + }; + + // Fire events from before track() triggred fire() + trackCallbacks.fire( mw.trackQueue ); + + /** + * Registry and firing of events. + * + * MediaWiki has various interface components that are extended, enhanced + * or manipulated in some other way by extensions, gadgets and even + * in core itself. + * + * This framework helps streamlining the timing of when these other + * code paths fire their plugins (instead of using document-ready, + * which can and should be limited to firing only once). + * + * Features like navigating to other wiki pages, previewing an edit + * and editing itself – without a refresh – can then retrigger these + * hooks accordingly to ensure everything still works as expected. + * + * Example usage: + * + * mw.hook( 'wikipage.content' ).add( fn ).remove( fn ); + * mw.hook( 'wikipage.content' ).fire( $content ); + * + * Handlers can be added and fired for arbitrary event names at any time. The same + * event can be fired multiple times. The last run of an event is memorized + * (similar to `$(document).ready` and `$.Deferred().done`). + * This means if an event is fired, and a handler added afterwards, the added + * function will be fired right away with the last given event data. + * + * Like Deferreds and Promises, the mw.hook object is both detachable and chainable. + * Thus allowing flexible use and optimal maintainability and authority control. + * You can pass around the `add` and/or `fire` method to another piece of code + * without it having to know the event name (or `mw.hook` for that matter). + * + * var h = mw.hook( 'bar.ready' ); + * new mw.Foo( .. ).fetch( { callback: h.fire } ); + * + * Note: Events are documented with an underscore instead of a dot in the event + * name due to jsduck not supporting dots in that position. + * + * @class mw.hook + */ + mw.hook = ( function () { + var lists = {}; + + /** + * Create an instance of mw.hook. + * + * @method hook + * @member mw + * @param {string} name Name of hook. + * @return {mw.hook} + */ + return function ( name ) { + var list = hasOwn.call( lists, name ) ? + lists[ name ] : + lists[ name ] = $.Callbacks( 'memory' ); + + return { + /** + * Register a hook handler + * + * @param {...Function} handler Function to bind. + * @chainable + */ + add: list.add, + + /** + * Unregister a hook handler + * + * @param {...Function} handler Function to unbind. + * @chainable + */ + remove: list.remove, + + /** + * Run a hook. + * + * @param {...Mixed} data + * @return {mw.hook} + * @chainable + */ + fire: function () { + return list.fireWith.call( this, null, slice.call( arguments ) ); + } + }; + }; + }() ); + + /** + * HTML construction helper functions + * + * @example + * + * var Html, output; + * + * Html = mw.html; + * output = Html.element( 'div', {}, new Html.Raw( + * Html.element( 'img', { src: '<' } ) + * ) ); + * mw.log( output ); //
+ * + * @class mw.html + * @singleton + */ + mw.html = ( function () { + function escapeCallback( s ) { + switch ( s ) { + case '\'': + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + } + } + + return { + /** + * Escape a string for HTML. + * + * Converts special characters to HTML entities. + * + * mw.html.escape( '< > \' & "' ); + * // Returns < > ' & " + * + * @param {string} s The string to escape + * @return {string} HTML + */ + escape: function ( s ) { + return s.replace( /['"<>&]/g, escapeCallback ); + }, + + /** + * Create an HTML element string, with safe escaping. + * + * @param {string} name The tag name. + * @param {Object} [attrs] An object with members mapping element names to values + * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element. + * + * - string: Text to be escaped. + * - null: The element is treated as void with short closing form, e.g. `
`. + * - this.Raw: The raw value is directly included. + * - this.Cdata: The raw value is directly included. An exception is + * thrown if it contains any illegal ETAGO delimiter. + * See . + * @return {string} HTML + */ + element: function ( name, attrs, contents ) { + var v, attrName, s = '<' + name; + + if ( attrs ) { + for ( attrName in attrs ) { + v = attrs[ attrName ]; + // Convert name=true, to name=name + if ( v === true ) { + v = attrName; + // Skip name=false + } else if ( v === false ) { + continue; + } + s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"'; + } + } + if ( contents === undefined || contents === null ) { + // Self close tag + s += '/>'; + return s; + } + // Regular open tag + s += '>'; + switch ( typeof contents ) { + case 'string': + // Escaped + s += this.escape( contents ); + break; + case 'number': + case 'boolean': + // Convert to string + s += String( contents ); + break; + default: + if ( contents instanceof this.Raw ) { + // Raw HTML inclusion + s += contents.value; + } else if ( contents instanceof this.Cdata ) { + // CDATA + if ( /<\/[a-zA-z]/.test( contents.value ) ) { + throw new Error( 'mw.html.element: Illegal end tag found in CDATA' ); + } + s += contents.value; + } else { + throw new Error( 'mw.html.element: Invalid type of contents' ); + } + } + s += ''; + return s; + }, + + /** + * Wrapper object for raw HTML passed to mw.html.element(). + * + * @class mw.html.Raw + * @constructor + * @param {string} value + */ + Raw: function ( value ) { + this.value = value; + }, + + /** + * Wrapper object for CDATA element contents passed to mw.html.element() + * + * @class mw.html.Cdata + * @constructor + * @param {string} value + */ + Cdata: function ( value ) { + this.value = value; + } + }; + }() ); + + /** + * Execute a function as soon as one or more required modules are ready. + * + * Example of inline dependency on OOjs: + * + * mw.loader.using( 'oojs', function () { + * OO.compare( [ 1 ], [ 1 ] ); + * } ); + * + * Example of inline dependency obtained via `require()`: + * + * mw.loader.using( [ 'mediawiki.util' ], function ( require ) { + * var util = require( 'mediawiki.util' ); + * } ); + * + * Since MediaWiki 1.23 this also returns a promise. + * + * Since MediaWiki 1.28 the promise is resolved with a `require` function. + * + * @member mw.loader + * @param {string|Array} dependencies Module name or array of modules names the + * callback depends on to be ready before executing + * @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} With a `require` function + */ + mw.loader.using = function ( dependencies, ready, error ) { + var deferred = $.Deferred(); + + // Allow calling with a single dependency as a string + if ( typeof dependencies === 'string' ) { + dependencies = [ dependencies ]; + } + + if ( ready ) { + deferred.done( ready ); + } + if ( error ) { + deferred.fail( error ); + } + + try { + // Resolve entire dependency map + dependencies = mw.loader.resolve( dependencies ); + } catch ( e ) { + return deferred.reject( e ).promise(); + } + + mw.loader.enqueue( dependencies, function () { + deferred.resolve( mw.loader.require ); + }, deferred.reject ); + + return deferred.promise(); + }; + + // Alias $j to jQuery for backwards compatibility + // @deprecated since 1.23 Use $ or jQuery instead + mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' ); +}() ); diff --git a/resources/src/mediawiki.base/mediawiki.errorLogger.js b/resources/src/mediawiki.base/mediawiki.errorLogger.js new file mode 100644 index 0000000000..e86aff6b78 --- /dev/null +++ b/resources/src/mediawiki.base/mediawiki.errorLogger.js @@ -0,0 +1,58 @@ +/** + * Try to catch errors in modules which don't do their own error handling. + * + * @class mw.errorLogger + * @singleton + */ +( function ( mw ) { + 'use strict'; + + mw.errorLogger = { + /** + * Fired via mw.track when an error is not handled by local code and is caught by the + * window.onerror handler. + * + * @event global_error + * @param {string} errorMessage Error errorMessage. + * @param {string} url URL where error was raised. + * @param {number} lineNumber Line number where error was raised. + * @param {number} [columnNumber] Line number where error was raised. Not all browsers + * support this. + * @param {Error|Mixed} [errorObject] The error object. Typically an instance of Error, but anything + * (even a primitive value) passed to a throw clause will end up here. + */ + + /** + * Install a window.onerror handler that will report via mw.track, while preserving + * any previous handler. + * + * @param {Object} window + */ + installGlobalHandler: function ( window ) { + // We will preserve the return value of the previous handler. window.onerror works the + // opposite way than normal event handlers (returning true will prevent the default + // action, returning false will let the browser handle the error normally, by e.g. + // logging to the console), so our fallback old handler needs to return false. + var oldHandler = window.onerror || function () { return false; }; + + /** + * Dumb window.onerror handler which forwards the errors via mw.track. + * + * @param {string} errorMessage + * @param {string} url + * @param {number} lineNumber + * @param {number} [columnNumber] + * @param {Error|Mixed} [errorObject] + * @return {boolean} True to prevent the default action + * @fires global_error + */ + window.onerror = function ( errorMessage, url, lineNumber, columnNumber, errorObject ) { + mw.track( 'global.error', { errorMessage: errorMessage, url: url, + lineNumber: lineNumber, columnNumber: columnNumber, errorObject: errorObject } ); + return oldHandler.apply( this, arguments ); + }; + } + }; + + mw.errorLogger.installGlobalHandler( window ); +}( mediaWiki ) ); diff --git a/resources/src/mediawiki/mediawiki.base.js b/resources/src/mediawiki/mediawiki.base.js deleted file mode 100644 index ed24af3065..0000000000 --- a/resources/src/mediawiki/mediawiki.base.js +++ /dev/null @@ -1,644 +0,0 @@ -/*! - * This file is currently loaded as part of the 'mediawiki' module and therefore - * concatenated to mediawiki.js and executed at the same time. This file exists - * to help prepare for splitting up the 'mediawiki' module. - * This effort is tracked at https://phabricator.wikimedia.org/T192623 - * - * In short: - * - * - mediawiki.js will be reduced to the minimum needed to define mw.loader and - * mw.config, and then moved to its own private "mediawiki.loader" module that - * can be embedded within the StartupModule response. - * - * - mediawiki.base.js and other files in this directory will remain part of the - * "mediawiki" module, and will remain a default/implicit dependency for all - * regular modules, just like jquery and wikibits already are. - */ -/* globals mw */ -( function () { - 'use strict'; - - var slice = Array.prototype.slice, - mwLoaderTrack = mw.track, - trackCallbacks = $.Callbacks( 'memory' ), - trackHandlers = [], - hasOwn = Object.prototype.hasOwnProperty; - - /** - * 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 - * @return {mw.Message} - * @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 ); - } - }; - - /** - * @class mw - * @singleton - */ - - /** - * @inheritdoc mw.inspect#runReports - * @method - */ - mw.inspect = function () { - var args = arguments; - mw.loader.using( 'mediawiki.inspect', function () { - mw.inspect.runReports.apply( mw.inspect, args ); - } ); - }; - - /** - * 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 - */ - mw.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; - } ); - }; - - // Expose Message constructor - mw.Message = Message; - - /** - * 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} - */ - mw.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} - */ - mw.msg = function () { - return mw.message.apply( mw.message, arguments ).toString(); - }; - - /** - * 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 - */ - mw.track = function ( topic, data ) { - mwLoaderTrack( topic, data ); - trackCallbacks.fire( mw.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] - */ - mw.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 - */ - mw.trackUnsubscribe = function ( callback ) { - trackHandlers = trackHandlers.filter( 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; - } ); - }; - - // Fire events from before track() triggred fire() - trackCallbacks.fire( mw.trackQueue ); - - /** - * Registry and firing of events. - * - * MediaWiki has various interface components that are extended, enhanced - * or manipulated in some other way by extensions, gadgets and even - * in core itself. - * - * This framework helps streamlining the timing of when these other - * code paths fire their plugins (instead of using document-ready, - * which can and should be limited to firing only once). - * - * Features like navigating to other wiki pages, previewing an edit - * and editing itself – without a refresh – can then retrigger these - * hooks accordingly to ensure everything still works as expected. - * - * Example usage: - * - * mw.hook( 'wikipage.content' ).add( fn ).remove( fn ); - * mw.hook( 'wikipage.content' ).fire( $content ); - * - * Handlers can be added and fired for arbitrary event names at any time. The same - * event can be fired multiple times. The last run of an event is memorized - * (similar to `$(document).ready` and `$.Deferred().done`). - * This means if an event is fired, and a handler added afterwards, the added - * function will be fired right away with the last given event data. - * - * Like Deferreds and Promises, the mw.hook object is both detachable and chainable. - * Thus allowing flexible use and optimal maintainability and authority control. - * You can pass around the `add` and/or `fire` method to another piece of code - * without it having to know the event name (or `mw.hook` for that matter). - * - * var h = mw.hook( 'bar.ready' ); - * new mw.Foo( .. ).fetch( { callback: h.fire } ); - * - * Note: Events are documented with an underscore instead of a dot in the event - * name due to jsduck not supporting dots in that position. - * - * @class mw.hook - */ - mw.hook = ( function () { - var lists = {}; - - /** - * Create an instance of mw.hook. - * - * @method hook - * @member mw - * @param {string} name Name of hook. - * @return {mw.hook} - */ - return function ( name ) { - var list = hasOwn.call( lists, name ) ? - lists[ name ] : - lists[ name ] = $.Callbacks( 'memory' ); - - return { - /** - * Register a hook handler - * - * @param {...Function} handler Function to bind. - * @chainable - */ - add: list.add, - - /** - * Unregister a hook handler - * - * @param {...Function} handler Function to unbind. - * @chainable - */ - remove: list.remove, - - /** - * Run a hook. - * - * @param {...Mixed} data - * @return {mw.hook} - * @chainable - */ - fire: function () { - return list.fireWith.call( this, null, slice.call( arguments ) ); - } - }; - }; - }() ); - - /** - * HTML construction helper functions - * - * @example - * - * var Html, output; - * - * Html = mw.html; - * output = Html.element( 'div', {}, new Html.Raw( - * Html.element( 'img', { src: '<' } ) - * ) ); - * mw.log( output ); //
- * - * @class mw.html - * @singleton - */ - mw.html = ( function () { - function escapeCallback( s ) { - switch ( s ) { - case '\'': - return '''; - case '"': - return '"'; - case '<': - return '<'; - case '>': - return '>'; - case '&': - return '&'; - } - } - - return { - /** - * Escape a string for HTML. - * - * Converts special characters to HTML entities. - * - * mw.html.escape( '< > \' & "' ); - * // Returns < > ' & " - * - * @param {string} s The string to escape - * @return {string} HTML - */ - escape: function ( s ) { - return s.replace( /['"<>&]/g, escapeCallback ); - }, - - /** - * Create an HTML element string, with safe escaping. - * - * @param {string} name The tag name. - * @param {Object} [attrs] An object with members mapping element names to values - * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element. - * - * - string: Text to be escaped. - * - null: The element is treated as void with short closing form, e.g. `
`. - * - this.Raw: The raw value is directly included. - * - this.Cdata: The raw value is directly included. An exception is - * thrown if it contains any illegal ETAGO delimiter. - * See . - * @return {string} HTML - */ - element: function ( name, attrs, contents ) { - var v, attrName, s = '<' + name; - - if ( attrs ) { - for ( attrName in attrs ) { - v = attrs[ attrName ]; - // Convert name=true, to name=name - if ( v === true ) { - v = attrName; - // Skip name=false - } else if ( v === false ) { - continue; - } - s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"'; - } - } - if ( contents === undefined || contents === null ) { - // Self close tag - s += '/>'; - return s; - } - // Regular open tag - s += '>'; - switch ( typeof contents ) { - case 'string': - // Escaped - s += this.escape( contents ); - break; - case 'number': - case 'boolean': - // Convert to string - s += String( contents ); - break; - default: - if ( contents instanceof this.Raw ) { - // Raw HTML inclusion - s += contents.value; - } else if ( contents instanceof this.Cdata ) { - // CDATA - if ( /<\/[a-zA-z]/.test( contents.value ) ) { - throw new Error( 'mw.html.element: Illegal end tag found in CDATA' ); - } - s += contents.value; - } else { - throw new Error( 'mw.html.element: Invalid type of contents' ); - } - } - s += ''; - return s; - }, - - /** - * Wrapper object for raw HTML passed to mw.html.element(). - * - * @class mw.html.Raw - * @constructor - * @param {string} value - */ - Raw: function ( value ) { - this.value = value; - }, - - /** - * Wrapper object for CDATA element contents passed to mw.html.element() - * - * @class mw.html.Cdata - * @constructor - * @param {string} value - */ - Cdata: function ( value ) { - this.value = value; - } - }; - }() ); - - /** - * Execute a function as soon as one or more required modules are ready. - * - * Example of inline dependency on OOjs: - * - * mw.loader.using( 'oojs', function () { - * OO.compare( [ 1 ], [ 1 ] ); - * } ); - * - * Example of inline dependency obtained via `require()`: - * - * mw.loader.using( [ 'mediawiki.util' ], function ( require ) { - * var util = require( 'mediawiki.util' ); - * } ); - * - * Since MediaWiki 1.23 this also returns a promise. - * - * Since MediaWiki 1.28 the promise is resolved with a `require` function. - * - * @member mw.loader - * @param {string|Array} dependencies Module name or array of modules names the - * callback depends on to be ready before executing - * @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} With a `require` function - */ - mw.loader.using = function ( dependencies, ready, error ) { - var deferred = $.Deferred(); - - // Allow calling with a single dependency as a string - if ( typeof dependencies === 'string' ) { - dependencies = [ dependencies ]; - } - - if ( ready ) { - deferred.done( ready ); - } - if ( error ) { - deferred.fail( error ); - } - - try { - // Resolve entire dependency map - dependencies = mw.loader.resolve( dependencies ); - } catch ( e ) { - return deferred.reject( e ).promise(); - } - - mw.loader.enqueue( dependencies, function () { - deferred.resolve( mw.loader.require ); - }, deferred.reject ); - - return deferred.promise(); - }; - - // Alias $j to jQuery for backwards compatibility - // @deprecated since 1.23 Use $ or jQuery instead - mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' ); -}() ); diff --git a/resources/src/mediawiki/mediawiki.errorLogger.js b/resources/src/mediawiki/mediawiki.errorLogger.js deleted file mode 100644 index e86aff6b78..0000000000 --- a/resources/src/mediawiki/mediawiki.errorLogger.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Try to catch errors in modules which don't do their own error handling. - * - * @class mw.errorLogger - * @singleton - */ -( function ( mw ) { - 'use strict'; - - mw.errorLogger = { - /** - * Fired via mw.track when an error is not handled by local code and is caught by the - * window.onerror handler. - * - * @event global_error - * @param {string} errorMessage Error errorMessage. - * @param {string} url URL where error was raised. - * @param {number} lineNumber Line number where error was raised. - * @param {number} [columnNumber] Line number where error was raised. Not all browsers - * support this. - * @param {Error|Mixed} [errorObject] The error object. Typically an instance of Error, but anything - * (even a primitive value) passed to a throw clause will end up here. - */ - - /** - * Install a window.onerror handler that will report via mw.track, while preserving - * any previous handler. - * - * @param {Object} window - */ - installGlobalHandler: function ( window ) { - // We will preserve the return value of the previous handler. window.onerror works the - // opposite way than normal event handlers (returning true will prevent the default - // action, returning false will let the browser handle the error normally, by e.g. - // logging to the console), so our fallback old handler needs to return false. - var oldHandler = window.onerror || function () { return false; }; - - /** - * Dumb window.onerror handler which forwards the errors via mw.track. - * - * @param {string} errorMessage - * @param {string} url - * @param {number} lineNumber - * @param {number} [columnNumber] - * @param {Error|Mixed} [errorObject] - * @return {boolean} True to prevent the default action - * @fires global_error - */ - window.onerror = function ( errorMessage, url, lineNumber, columnNumber, errorObject ) { - mw.track( 'global.error', { errorMessage: errorMessage, url: url, - lineNumber: lineNumber, columnNumber: columnNumber, errorObject: errorObject } ); - return oldHandler.apply( this, arguments ); - }; - } - }; - - mw.errorLogger.installGlobalHandler( window ); -}( mediaWiki ) );