From 08aa4418c30cfc18ccc69a0f0f9cb9e17be6c196 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Mon, 12 Aug 2013 09:28:15 +0200 Subject: Update to MediaWiki 1.21.1 --- resources/mediawiki/mediawiki.Title.js | 103 ++-- resources/mediawiki/mediawiki.Uri.js | 64 ++- resources/mediawiki/mediawiki.debug.css | 1 - resources/mediawiki/mediawiki.debug.js | 6 +- resources/mediawiki/mediawiki.feedback.js | 45 +- resources/mediawiki/mediawiki.hidpi.js | 5 + resources/mediawiki/mediawiki.htmlform.js | 112 ++-- resources/mediawiki/mediawiki.jqueryMsg.js | 423 ++++++++++---- resources/mediawiki/mediawiki.jqueryMsg.peg | 1 + resources/mediawiki/mediawiki.js | 699 ++++++++++++++---------- resources/mediawiki/mediawiki.log.js | 2 +- resources/mediawiki/mediawiki.notification.js | 145 ++--- resources/mediawiki/mediawiki.notify.js | 13 +- resources/mediawiki/mediawiki.searchSuggest.css | 16 + resources/mediawiki/mediawiki.searchSuggest.js | 110 +++- resources/mediawiki/mediawiki.user.js | 25 +- resources/mediawiki/mediawiki.util.js | 236 ++++---- 17 files changed, 1218 insertions(+), 788 deletions(-) create mode 100644 resources/mediawiki/mediawiki.hidpi.js create mode 100644 resources/mediawiki/mediawiki.searchSuggest.css (limited to 'resources/mediawiki') diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js index 33cca585..b86a14ba 100644 --- a/resources/mediawiki/mediawiki.Title.js +++ b/resources/mediawiki/mediawiki.Title.js @@ -1,6 +1,4 @@ -/** - * mediaWiki.Title - * +/*! * @author Neil Kandalgaonkar, 2010 * @author Timo Tijhof, 2011 * @since 1.18 @@ -12,13 +10,12 @@ /* Local space */ /** - * Title - * @constructor + * @class mw.Title * - * @param title {String} Title of the page. If no second argument given, + * @constructor + * @param {string} title Title of the page. If no second argument given, * this will be searched for a namespace. - * @param namespace {Number} (optional) Namespace id. If given, title will be taken as-is. - * @return {Title} this + * @param {number} [namespace] Namespace id. If given, title will be taken as-is. */ function Title( title, namespace ) { this.ns = 0; // integer namespace id @@ -35,17 +32,16 @@ } var - /** - * Public methods (defined later) - */ + /* Public methods (defined later) */ fn, /** * Strip some illegal chars: control chars, colon, less than, greater than, * brackets, braces, pipe, whitespace and normal spaces. This still leaves some insanity * intact, like unicode bidi chars, but it's a good start.. - * @param s {String} - * @return {String} + * @ignore + * @param {string} s + * @return {string} */ clean = function ( s ) { if ( s !== undefined ) { @@ -55,8 +51,9 @@ var /** * Convert db-key to readable text. - * @param s {String} - * @return {String} + * @ignore + * @param {string} s + * @return {string} */ text = function ( s ) { if ( s !== null && s !== undefined ) { @@ -68,13 +65,15 @@ var /** * Sanitize name. + * @ignore */ fixName = function ( s ) { return clean( $.trim( s ) ); }, /** - * Sanitize name. + * Sanitize extension. + * @ignore */ fixExt = function ( s ) { return clean( s ); @@ -82,6 +81,7 @@ var /** * Sanitize namespace id. + * @ignore * @param id {Number} Namespace id. * @return {Number|Boolean} The id as-is or boolean false if invalid. */ @@ -99,8 +99,8 @@ var /** * Get namespace id from namespace name by any known namespace/id pair (localized, canonical or alias). - * - * @example On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'. + * Example: On a German wiki this would return 6 for any of 'File', 'Datei', 'Image' or even 'Bild'. + * @ignore * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored). * @return {Number|Boolean} Namespace id or boolean false if unrecognized. */ @@ -125,19 +125,20 @@ var /** * Helper to extract namespace, name and extension from a string. * - * @param title {mw.Title} - * @param raw {String} + * @ignore + * @param {mw.Title} title + * @param {string} raw * @return {mw.Title} */ setAll = function ( title, s ) { // In normal browsers the match-array contains null/undefined if there's no match, // IE returns an empty string. var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ), - ns_match = getNsIdByName( matches[1] ); + nsMatch = getNsIdByName( matches[1] ); // Namespace must be valid, and title must be a non-empty string. - if ( ns_match && typeof matches[2] === 'string' && matches[2] !== '' ) { - title.ns = ns_match; + if ( nsMatch && typeof matches[2] === 'string' && matches[2] !== '' ) { + title.ns = nsMatch; title.name = fixName( matches[2] ); if ( typeof matches[3] === 'string' && matches[3] !== '' ) { title.ext = fixExt( matches[3] ); @@ -153,8 +154,9 @@ var /** * Helper to extract name and extension from a string. * - * @param title {mw.Title} - * @param raw {String} + * @ignore + * @param {mw.Title} title + * @param {string} raw * @return {mw.Title} */ setNameAndExtension = function ( title, raw ) { @@ -179,8 +181,9 @@ var /** * Whether this title exists on the wiki. - * @param title {mixed} prefixed db-key name (string) or instance of Title - * @return {mixed} Boolean true/false if the information is available. Otherwise null. + * @static + * @param {Mixed} title prefixed db-key name (string) or instance of Title + * @return {Mixed} Boolean true/false if the information is available. Otherwise null. */ Title.exists = function ( title ) { var type = $.type( title ), obj = Title.exist.pages, match; @@ -198,20 +201,27 @@ var }; /** - * @var Title.exist {Object} + * @static + * @property */ Title.exist = { /** - * @var Title.exist.pages {Object} Keyed by PrefixedDb title. + * @static + * @property {Object} exist.pages Keyed by PrefixedDb title. * Boolean true value indicates page does exist. */ pages: {}, /** - * @example Declare existing titles: Title.exist.set(['User:John_Doe', ...]); - * @example Declare titles nonexistent: Title.exist.set(['File:Foo_bar.jpg', ...], false); - * @param titles {String|Array} Title(s) in strict prefixedDb title form. - * @param state {Boolean} (optional) State of the given titles. Defaults to true. - * @return {Boolean} + * Example to declare existing titles: + * Title.exist.set(['User:John_Doe', ...]); + * Eample to declare titles nonexistent: + * Title.exist.set(['File:Foo_bar.jpg', ...], false); + * + * @static + * @property exist.set + * @param {string|Array} titles Title(s) in strict prefixedDb title form. + * @param {boolean} [state] State of the given titles. Defaults to true. + * @return {boolean} */ set: function ( titles, state ) { titles = $.isArray( titles ) ? titles : [titles]; @@ -231,7 +241,7 @@ var /** * Get the namespace number. - * @return {Number} + * @return {number} */ getNamespaceId: function (){ return this.ns; @@ -240,7 +250,7 @@ var /** * Get the namespace prefix (in the content-language). * In NS_MAIN this is '', otherwise namespace name plus ':' - * @return {String} + * @return {string} */ getNamespacePrefix: function (){ return mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':'); @@ -248,7 +258,7 @@ var /** * The name, like "Foo_bar" - * @return {String} + * @return {string} */ getName: function () { if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { @@ -260,7 +270,7 @@ var /** * The name, like "Foo bar" - * @return {String} + * @return {string} */ getNameText: function () { return text( this.getName() ); @@ -269,6 +279,7 @@ var /** * Get full name in prefixed DB form, like File:Foo_bar.jpg, * most useful for API calls, anything that must identify the "title". + * @return {string} */ getPrefixedDb: function () { return this.getNamespacePrefix() + this.getMain(); @@ -276,7 +287,7 @@ var /** * Get full name in text form, like "File:Foo bar.jpg". - * @return {String} + * @return {string} */ getPrefixedText: function () { return text( this.getPrefixedDb() ); @@ -284,7 +295,7 @@ var /** * The main title (without namespace), like "Foo_bar.jpg" - * @return {String} + * @return {string} */ getMain: function () { return this.getName() + this.getDotExtension(); @@ -292,7 +303,7 @@ var /** * The "text" form, like "Foo bar.jpg" - * @return {String} + * @return {string} */ getMainText: function () { return text( this.getMain() ); @@ -300,7 +311,7 @@ var /** * Get the extension (returns null if there was none) - * @return {String|null} extension + * @return {string|null} */ getExtension: function () { return this.ext; @@ -308,7 +319,7 @@ var /** * Convenience method: return string like ".jpg", or "" if no extension - * @return {String} + * @return {string} */ getDotExtension: function () { return this.ext === null ? '' : '.' + this.ext; @@ -316,7 +327,8 @@ var /** * Return the URL to this title - * @return {String} + * @see mw.util#wikiGetlink + * @return {string} */ getUrl: function () { return mw.util.wikiGetlink( this.toString() ); @@ -324,7 +336,8 @@ var /** * Whether this title exists on the wiki. - * @return {mixed} Boolean true/false if the information is available. Otherwise null. + * @see #static-method-exists + * @return {boolean|null} If the information is available. Otherwise null. */ exists: function () { return Title.exists( this ); diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js index bd12b214..643e5c3e 100644 --- a/resources/mediawiki/mediawiki.Uri.js +++ b/resources/mediawiki/mediawiki.Uri.js @@ -61,11 +61,11 @@ /** * Function that's useful when constructing the URI string -- we frequently encounter the pattern of * having to add something to the URI as we go, but only if it's present, and to include a character before or after if so. - * @param {String} to prepend, if value not empty - * @param {String} value to include, if not empty - * @param {String} to append, if value not empty - * @param {Boolean} raw -- if true, do not URI encode - * @return {String} + * @param {string|undefined} pre To prepend. + * @param {string} val To include. + * @param {string} post To append. + * @param {boolean} raw If true, val will not be encoded. + * @return {string} Result. */ function cat( pre, val, post, raw ) { if ( val === undefined || val === null || val === '' ) { @@ -76,8 +76,8 @@ // Regular expressions to parse many common URIs. var parser = { - strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/, - loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/ + strict: /^(?:([^:\/?#]+):)?(?:\/\/(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?)?((?:[^?#\/]*\/)*[^?#]*)(?:\?([^#]*))?(?:#(.*))?/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?(?:(?:([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?([^:\/?#]*)(?::(\d*))?((?:\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?[^?#\/]*)(?:\?([^#]*))?(?:#(.*))?/ }, // The order here matches the order of captured matches in the above parser regexes. @@ -103,14 +103,14 @@ /** * Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse. * @constructor - * @param {Object|String} URI string, or an Object with appropriate properties (especially another URI object to clone). + * @param {Object|string} uri URI string, or an Object with appropriate properties (especially another URI object to clone). * Object must have non-blank 'protocol', 'host', and 'path' properties. - * This parameter is optional. If omitted (or set to undefined, null or empty string), then an object will be created - * for the default uri of this constructor (e.g. document.location for mw.Uri in MediaWiki core). - * @param {Object|Boolean} Object with options, or (backwards compatibility) a boolean for strictMode - * - strictMode {Boolean} Trigger strict mode parsing of the url. Default: false - * - overrideKeys {Boolean} Wether to let duplicate query parameters override eachother (true) or automagically - * convert to an array (false, default). + * This parameter is optional. If omitted (or set to undefined, null or empty string), then an object will be created + * for the default uri of this constructor (e.g. document.location for mw.Uri in MediaWiki core). + * @param {Object|boolean} Object with options, or (backwards compatibility) a boolean for strictMode + * - {boolean} strictMode Trigger strict mode parsing of the url. Default: false + * - {boolean} overrideKeys Wether to let duplicate query parameters override eachother (true) or automagically + * convert to an array (false, default). */ function Uri( uri, options ) { options = typeof options === 'object' ? options : { strictMode: !!options }; @@ -158,7 +158,7 @@ } if ( this.path && this.path.charAt( 0 ) !== '/' ) { // A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot - // figure out whether the last path compoennt of defaultUri.path is a directory or a file. + // figure out whether the last path component of defaultUri.path is a directory or a file. throw new Error( 'Bad constructor arguments' ); } if ( !( this.protocol && this.host && this.path ) ) { @@ -169,8 +169,8 @@ /** * Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more compliant with RFC 3986 * Similar to rawurlencode from PHP and our JS library mw.util.rawurlencode, but we also replace space with a + - * @param {String} string - * @return {String} encoded for URI + * @param {string} s String to encode. + * @return {string} Encoded string for URI. */ Uri.encode = function ( s ) { return encodeURIComponent( s ) @@ -180,9 +180,9 @@ }; /** - * Standard decodeURIComponent, with '+' to space - * @param {String} string encoded for URI - * @return {String} decoded string + * Standard decodeURIComponent, with '+' to space. + * @param {string} s String encoded for URI. + * @return {string} Decoded string. */ Uri.decode = function ( s ) { return decodeURIComponent( s.replace( /\+/g, '%20' ) ); @@ -192,9 +192,9 @@ /** * Parse a string and set our properties accordingly. - * @param {String} URI + * @param {string} str URI * @param {Object} options - * @return {Boolean} success + * @return {boolean} Success. */ parse: function ( str, options ) { var q, @@ -240,7 +240,7 @@ /** * Returns user and password portion of a URI. - * @return {String} + * @return {string} */ getUserInfo: function () { return cat( '', this.user, cat( ':', this.password, '' ) ); @@ -248,7 +248,7 @@ /** * Gets host and port portion of a URI. - * @return {String} + * @return {string} */ getHostPort: function () { return this.host + cat( ':', this.port, '' ); @@ -257,7 +257,7 @@ /** * Returns the userInfo and host and port portion of the URI. * In most real-world URLs, this is simply the hostname, but it is more general. - * @return {String} + * @return {string} */ getAuthority: function () { return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); @@ -266,7 +266,7 @@ /** * Returns the query arguments of the URL, encoded into a string * Does not preserve the order of arguments passed into the URI. Does handle escaping. - * @return {String} + * @return {string} */ getQueryString: function () { var args = []; @@ -274,7 +274,13 @@ var k = Uri.encode( key ), vals = $.isArray( val ) ? val : [ val ]; $.each( vals, function ( i, v ) { - args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) ); + if ( v === null ) { + args.push( k ); + } else if ( k === 'title' ) { + args.push( k + '=' + mw.util.wikiUrlencode( v ) ); + } else { + args.push( k + '=' + Uri.encode( v ) ); + } } ); } ); return args.join( '&' ); @@ -282,7 +288,7 @@ /** * Returns everything after the authority section of the URI - * @return {String} + * @return {string} */ getRelativePath: function () { return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); @@ -290,7 +296,7 @@ /** * Gets the entire URI string. May not be precisely the same as input due to order of query arguments. - * @return {String} the URI string + * @return {string} The URI string. */ toString: function () { return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); diff --git a/resources/mediawiki/mediawiki.debug.css b/resources/mediawiki/mediawiki.debug.css index 149e1bff..513cb847 100644 --- a/resources/mediawiki/mediawiki.debug.css +++ b/resources/mediawiki/mediawiki.debug.css @@ -1,6 +1,5 @@ .mw-debug { width: 100%; - text-align: left; background-color: #eee; border-top: 1px solid #aaa; } diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js index 1ad1a623..88af3c65 100644 --- a/resources/mediawiki/mediawiki.debug.js +++ b/resources/mediawiki/mediawiki.debug.js @@ -96,7 +96,7 @@ buildHtml: function () { var $container, $bits, panes, id, gitInfo; - $container = $( '
' ); + $container = $( '
' ); $bits = $( '
' ); @@ -187,9 +187,7 @@ .text( 'Time: ' + this.data.time.toFixed( 5 ) ); bitDiv( 'memory' ) - .text( 'Memory: ' + this.data.memory ) - .append( $( '' ).text( ' (' + this.data.memoryPeak + ')' ) ); - + .text( 'Memory: ' + this.data.memory + ' (Peak: ' + this.data.memoryPeak + ')' ); $bits.appendTo( $container ); diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js index 634d02b1..1afe51ef 100644 --- a/resources/mediawiki/mediawiki.feedback.js +++ b/resources/mediawiki/mediawiki.feedback.js @@ -1,5 +1,5 @@ /** - * mediawiki.Feedback + * mediawiki.feedback * * @author Ryan Kaldari, 2010 * @author Neil Kandalgaonkar, 2010-11 @@ -68,17 +68,28 @@ mw.Feedback.prototype = { setup: function () { - var fb = this; + var $feedbackPageLink, + $bugNoteLink, + $bugsListLink, + fb = this; - var $feedbackPageLink = $( '' ) - .attr( { 'href': fb.title.getUrl(), 'target': '_blank' } ) - .css( { 'white-space': 'nowrap' } ); + $feedbackPageLink = $( '' ) + .attr( { + href: fb.title.getUrl(), + target: '_blank' + } ) + .css( { + whiteSpace: 'nowrap' + } ); - var $bugNoteLink = $( '' ).attr( { 'href': '#' } ).click( function () { + $bugNoteLink = $( '' ).attr( { href: '#' } ).click( function () { fb.displayBugs(); } ); - var $bugsListLink = $( '' ).attr( { 'href': fb.bugsListLink, 'target': '_blank' } ); + $bugsListLink = $( '' ).attr( { + href: fb.bugsListLink, + target: '_blank' + } ); // TODO: Use a stylesheet instead of these inline styles this.$dialog = @@ -108,7 +119,7 @@ ), $( '' ).append( mw.msg( 'feedback-adding' ), - $( '
' ), + $( '
' ), $( '' ) ), $( '' ).msg( @@ -148,9 +159,9 @@ }, displayBugs: function () { - var fb = this; + var fb = this, + bugsButtons = {}; this.display( 'bugs' ); - var bugsButtons = {}; bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () { window.open( fb.bugsLink, '_blank' ); }; @@ -163,9 +174,9 @@ }, displayThanks: function () { - var fb = this; + var fb = this, + closeButton = {}; this.display( 'thanks' ); - var closeButton = {}; closeButton[ mw.msg( 'feedback-close' ) ] = function () { fb.$dialog.dialog( 'close' ); }; @@ -181,14 +192,14 @@ * message: {String} */ displayForm: function ( contents ) { - var fb = this; + var fb = this, + formButtons = {}; this.subjectInput.value = ( contents && contents.subject ) ? contents.subject : ''; this.messageInput.value = ( contents && contents.message ) ? contents.message : ''; this.display( 'form' ); // Set up buttons for dialog box. We have to do it the hard way since the json keys are localized - var formButtons = {}; formButtons[ mw.msg( 'feedback-submit' ) ] = function () { fb.submit(); }; @@ -199,10 +210,10 @@ }, displayError: function ( message ) { - var fb = this; + var fb = this, + closeButton = {}; this.display( 'error' ); this.$dialog.find( '.feedback-error-msg' ).msg( message ); - var closeButton = {}; closeButton[ mw.msg( 'feedback-close' ) ] = function () { fb.$dialog.dialog( 'close' ); }; @@ -231,7 +242,7 @@ } } - function err( code, info ) { + function err() { // ajax request failed fb.displayError( 'feedback-error3' ); } diff --git a/resources/mediawiki/mediawiki.hidpi.js b/resources/mediawiki/mediawiki.hidpi.js new file mode 100644 index 00000000..ecee450c --- /dev/null +++ b/resources/mediawiki/mediawiki.hidpi.js @@ -0,0 +1,5 @@ +jQuery( function ( $ ) { + // Apply hidpi images on DOM-ready + // Some may have already partly preloaded at low resolution. + $( 'body' ).hidpi(); +} ); diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js index a4753b99..83bf2e3a 100644 --- a/resources/mediawiki/mediawiki.htmlform.js +++ b/resources/mediawiki/mediawiki.htmlform.js @@ -1,64 +1,62 @@ /** - * Utility functions for jazzing up HTMLForm elements + * Utility functions for jazzing up HTMLForm elements. */ ( function ( $ ) { -/** - * jQuery plugin to fade or snap to visible state. - * - * @param boolean instantToggle (optional) - * @return jQuery - */ -$.fn.goIn = function ( instantToggle ) { - if ( instantToggle === true ) { - return $(this).show(); - } - return $(this).stop( true, true ).fadeIn(); -}; - -/** - * jQuery plugin to fade or snap to hiding state. - * - * @param boolean instantToggle (optional) - * @return jQuery - */ -$.fn.goOut = function ( instantToggle ) { - if ( instantToggle === true ) { - return $(this).hide(); - } - return $(this).stop( true, true ).fadeOut(); -}; - -/** - * Bind a function to the jQuery object via live(), and also immediately trigger - * the function on the objects with an 'instant' parameter set to true - * @param callback function taking one parameter, which is Bool true when the event - * is called immediately, and the EventArgs object when triggered from an event - */ -$.fn.liveAndTestAtStart = function ( callback ){ - $(this) - .live( 'change', callback ) - .each( function ( index, element ){ - callback.call( this, true ); - } ); -}; - -// Document ready: -$( function () { - - // Animate the SelectOrOther fields, to only show the text field when - // 'other' is selected. - $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { - var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); - $other = $other.add( $other.siblings( 'br' ) ); - if ( $(this).val() === 'other' ) { - $other.goIn( instant ); - } else { - $other.goOut( instant ); + /** + * jQuery plugin to fade or snap to visible state. + * + * @param {boolean} instantToggle [optional] + * @return {jQuery} + */ + $.fn.goIn = function ( instantToggle ) { + if ( instantToggle === true ) { + return $(this).show(); } - }); - -}); - + return $(this).stop( true, true ).fadeIn(); + }; + + /** + * jQuery plugin to fade or snap to hiding state. + * + * @param {boolean} instantToggle [optional] + * @return jQuery + */ + $.fn.goOut = function ( instantToggle ) { + if ( instantToggle === true ) { + return $(this).hide(); + } + return $(this).stop( true, true ).fadeOut(); + }; + + /** + * Bind a function to the jQuery object via live(), and also immediately trigger + * the function on the objects with an 'instant' parameter set to true. + * @param {Function} callback Takes one parameter, which is {true} when the + * event is called immediately, and {jQuery.Event} when triggered from an event. + */ + $.fn.liveAndTestAtStart = function ( callback ){ + $(this) + .live( 'change', callback ) + .each( function () { + callback.call( this, true ); + } ); + }; + + $( function () { + + // Animate the SelectOrOther fields, to only show the text field when + // 'other' is selected. + $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { + var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); + $other = $other.add( $other.siblings( 'br' ) ); + if ( $(this).val() === 'other' ) { + $other.goIn( instant ); + } else { + $other.goOut( instant ); + } + }); + + } ); }( jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 86af31ff..183b525e 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -5,13 +5,26 @@ * @author neilk@wikimedia.org */ ( function ( mw, $ ) { - var slice = Array.prototype.slice, + var oldParser, + slice = Array.prototype.slice, parserDefaults = { magic : { 'SITENAME' : mw.config.get( 'wgSiteName' ) }, messages : mw.messages, - language : mw.language + language : mw.language, + + // Same meaning as in mediawiki.js. + // + // Only 'text', 'parse', and 'escaped' are supported, and the + // actual escaping for 'escaped' is done by other code (generally + // through jqueryMsg). + // + // However, note that this default only + // applies to direct calls to jqueryMsg. The default for mediawiki.js itself + // is 'text', including when it uses jqueryMsg. + format: 'parse' + }; /** @@ -30,8 +43,8 @@ * @return {jQuery} */ return function ( args ) { - var key = args[0]; - var argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); + var key = args[0], + argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); try { return parser.parse( key, argsArray ); } catch ( e ) { @@ -56,19 +69,32 @@ * @return {Function} function suitable for assigning to window.gM */ mw.jqueryMsg.getMessageFunction = function ( options ) { - var failableParserFn = getFailableParserFn( options ); + var failableParserFn = getFailableParserFn( options ), + format; + + if ( options && options.format !== undefined ) { + format = options.format; + } else { + format = parserDefaults.format; + } + /** * N.B. replacements are variadic arguments or an array in second parameter. In other words: * somefunction(a, b, c, d) * is equivalent to * somefunction(a, [b, c, d]) * - * @param {String} message key - * @param {Array} optional replacements (can also specify variadically) - * @return {String} rendered HTML as string + * @param {string} key Message key. + * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). + * @return {string} Rendered HTML. */ - return function ( /* key, replacements */ ) { - return failableParserFn( arguments ).html(); + return function () { + var failableResult = failableParserFn( arguments ); + if ( format === 'text' || format === 'escaped' ) { + return failableResult.text(); + } else { + return failableResult.html(); + } }; }; @@ -93,12 +119,14 @@ * somefunction(a, [b, c, d]) * * We append to 'this', which in a jQuery plugin context will be the selected elements. - * @param {String} message key - * @param {Array} optional replacements (can also specify variadically) + * @param {string} key Message key. + * @param {Array|mixed} replacements Optional variable replacements (variadically or an array). * @return {jQuery} this */ - return function ( /* key, replacements */ ) { + return function () { var $target = this.empty(); + // TODO: Simply $target.append( failableParserFn( arguments ).contents() ) + // or Simply $target.append( failableParserFn( arguments ) ) $.each( failableParserFn( arguments ).contents(), function ( i, node ) { $target.append( node ); } ); @@ -113,20 +141,36 @@ */ mw.jqueryMsg.parser = function ( options ) { this.settings = $.extend( {}, parserDefaults, options ); + this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' ); + this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic ); }; mw.jqueryMsg.parser.prototype = { - // cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical. - // (This is why we would like to move this functionality server-side). + /** + * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, to the AST of the message. + * + * In most cases, the message is a string so this is identical. + * (This is why we would like to move this functionality server-side). + * + * The two parts of the key are separated by colon. For example: + * + * "message-key:true": ast + * + * if they key is "message-key" and onlyCurlyBraceTransform is true. + * + * This cache is shared by all instances of mw.jqueryMsg.parser. + * + * @static + */ astCache: {}, /** * Where the magic happens. * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery * If an error is thrown, returns original key, and logs the error - * @param {String} message key - * @param {Array} replacements for $1, $2... $n + * @param {String} key Message key. + * @param {Array} replacements Variable replacements for $1, $2... $n * @return {jQuery} */ parse: function ( key, replacements ) { @@ -139,16 +183,19 @@ * @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing */ getAst: function ( key ) { - if ( this.astCache[ key ] === undefined ) { - var wikiText = this.settings.messages.get( key ); + var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText; + + if ( this.astCache[ cacheKey ] === undefined ) { + wikiText = this.settings.messages.get( key ); if ( typeof wikiText !== 'string' ) { - wikiText = "\\[" + key + "\\]"; + wikiText = '\\[' + key + '\\]'; } - this.astCache[ key ] = this.wikiTextToAst( wikiText ); + this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText ); } - return this.astCache[ key ]; + return this.astCache[ cacheKey ]; }, - /* + + /** * Parses the input wikiText into an abstract syntax tree, essentially an s-expression. * * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already. @@ -159,18 +206,27 @@ * @return {Mixed} abstract syntax tree */ wikiTextToAst: function ( input ) { + var pos, + regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets, + backslash, anyCharacter, escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral, + whitespace, dollar, digits, + openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon, + templateContents, openTemplate, closeTemplate, + nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result; // Indicates current position in input as we parse through it. // Shared among all parsing functions below. - var pos = 0; + pos = 0; + // ========================================================= // parsing combinators - could be a library on its own // ========================================================= // Try parsers until one works, if none work return null function choice( ps ) { return function () { - for ( var i = 0; i < ps.length; i++ ) { - var result = ps[i](); + var i, result; + for ( i = 0; i < ps.length; i++ ) { + result = ps[i](); if ( result !== null ) { return result; } @@ -181,10 +237,11 @@ // try several ps in a row, all must succeed or return null // this is the only eager one function sequence( ps ) { - var originalPos = pos; - var result = []; - for ( var i = 0; i < ps.length; i++ ) { - var res = ps[i](); + var i, res, + originalPos = pos, + result = []; + for ( i = 0; i < ps.length; i++ ) { + res = ps[i](); if ( res === null ) { pos = originalPos; return null; @@ -197,9 +254,9 @@ // must succeed a minimum of n times or return null function nOrMore( n, p ) { return function () { - var originalPos = pos; - var result = []; - var parsed = p(); + var originalPos = pos, + result = [], + parsed = p(); while ( parsed !== null ) { result.push( parsed ); parsed = p(); @@ -258,11 +315,12 @@ // but some debuggers can't tell you exactly where they come from. Also the mutually // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF) // This may be because, to save code, memoization was removed - var regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); - var regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); - var regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); - var backslash = makeStringParser( "\\" ); - var anyCharacter = makeRegexParser( /^./ ); + regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); + regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); + regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); + regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ ); + backslash = makeStringParser( '\\' ); + anyCharacter = makeRegexParser( /^./ ); function escapedLiteral() { var result = sequence( [ backslash, @@ -270,36 +328,50 @@ ] ); return result === null ? null : result[1]; } - var escapedOrLiteralWithoutSpace = choice( [ + escapedOrLiteralWithoutSpace = choice( [ escapedLiteral, regularLiteralWithoutSpace ] ); - var escapedOrLiteralWithoutBar = choice( [ + escapedOrLiteralWithoutBar = choice( [ escapedLiteral, regularLiteralWithoutBar ] ); - var escapedOrRegularLiteral = choice( [ + escapedOrRegularLiteral = choice( [ escapedLiteral, regularLiteral ] ); // Used to define "literals" without spaces, in space-delimited situations function literalWithoutSpace() { - var result = nOrMore( 1, escapedOrLiteralWithoutSpace )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrLiteralWithoutSpace )(); + return result === null ? null : result.join(''); } // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default // it is not a literal in the parameter function literalWithoutBar() { - var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrLiteralWithoutBar )(); + return result === null ? null : result.join(''); } + + // Used for wikilink page names. Like literalWithoutBar, but + // without allowing escapes. + function unescapedLiteralWithoutBar() { + var result = nOrMore( 1, regularLiteralWithoutBar )(); + return result === null ? null : result.join(''); + } + function literal() { - var result = nOrMore( 1, escapedOrRegularLiteral )(); - return result === null ? null : result.join(''); + var result = nOrMore( 1, escapedOrRegularLiteral )(); + return result === null ? null : result.join(''); } - var whitespace = makeRegexParser( /^\s+/ ); - var dollar = makeStringParser( '$' ); - var digits = makeRegexParser( /^\d+/ ); + + function curlyBraceTransformExpressionLiteral() { + var result = nOrMore( 1, regularLiteralWithSquareBrackets )(); + return result === null ? null : result.join(''); + } + + whitespace = makeRegexParser( /^\s+/ ); + dollar = makeStringParser( '$' ); + digits = makeRegexParser( /^\d+/ ); function replacement() { var result = sequence( [ @@ -311,12 +383,13 @@ } return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ]; } - var openExtlink = makeStringParser( '[' ); - var closeExtlink = makeStringParser( ']' ); + openExtlink = makeStringParser( '[' ); + closeExtlink = makeStringParser( ']' ); // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed function extlink() { - var result = null; - var parsedResult = sequence( [ + var result, parsedResult; + result = null; + parsedResult = sequence( [ openExtlink, nonWhitespaceExpression, whitespace, @@ -343,39 +416,73 @@ } return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; } - var openLink = makeStringParser( '[[' ); - var closeLink = makeStringParser( ']]' ); + openLink = makeStringParser( '[[' ); + closeLink = makeStringParser( ']]' ); + pipe = makeStringParser( '|' ); + + function template() { + var result = sequence( [ + openTemplate, + templateContents, + closeTemplate + ] ); + return result === null ? null : result[1]; + } + + wikilinkPage = choice( [ + unescapedLiteralWithoutBar, + template + ] ); + + function pipedWikilink() { + var result = sequence( [ + wikilinkPage, + pipe, + expression + ] ); + return result === null ? null : [ result[0], result[2] ]; + } + + wikilinkContents = choice( [ + pipedWikilink, + wikilinkPage // unpiped link + ] ); + function link() { - var result = null; - var parsedResult = sequence( [ + var result, parsedResult, parsedLinkContents; + result = null; + + parsedResult = sequence( [ openLink, - expression, + wikilinkContents, closeLink ] ); if ( parsedResult !== null ) { - result = [ 'WLINK', parsedResult[1] ]; + parsedLinkContents = parsedResult[1]; + result = [ 'WLINK' ].concat( parsedLinkContents ); } return result; } - var templateName = transform( + templateName = transform( // see $wgLegalTitleChars // not allowing : due to the need to catch "PLURAL:$1" makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ), function ( result ) { return result.toString(); } ); function templateParam() { - var result = sequence( [ + var expr, result; + result = sequence( [ pipe, nOrMore( 0, paramExpression ) ] ); if ( result === null ) { return null; } - var expr = result[1]; - // use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw. - return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0]; + expr = result[1]; + // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw. + return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0]; } - var pipe = makeStringParser( '|' ); + function templateWithReplacement() { var result = sequence( [ templateName, @@ -392,8 +499,8 @@ ] ); return result === null ? null : [ result[0], result[2] ]; } - var colon = makeStringParser(':'); - var templateContents = choice( [ + colon = makeStringParser(':'); + templateContents = choice( [ function () { var res = sequence( [ // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}} @@ -414,17 +521,9 @@ return [ res[0] ].concat( res[1] ); } ] ); - var openTemplate = makeStringParser('{{'); - var closeTemplate = makeStringParser('}}'); - function template() { - var result = sequence( [ - openTemplate, - templateContents, - closeTemplate - ] ); - return result === null ? null : result[1]; - } - var nonWhitespaceExpression = choice( [ + openTemplate = makeStringParser('{{'); + closeTemplate = makeStringParser('}}'); + nonWhitespaceExpression = choice( [ template, link, extLinkParam, @@ -432,7 +531,7 @@ replacement, literalWithoutSpace ] ); - var paramExpression = choice( [ + paramExpression = choice( [ template, link, extLinkParam, @@ -440,7 +539,8 @@ replacement, literalWithoutBar ] ); - var expression = choice( [ + + expression = choice( [ template, link, extLinkParam, @@ -448,25 +548,42 @@ replacement, literal ] ); - function start() { - var result = nOrMore( 0, expression )(); + + // Used when only {{-transformation is wanted, for 'text' + // or 'escaped' formats + curlyBraceTransformExpression = choice( [ + template, + replacement, + curlyBraceTransformExpressionLiteral + ] ); + + + /** + * Starts the parse + * + * @param {Function} rootExpression root parse function + */ + function start( rootExpression ) { + var result = nOrMore( 0, rootExpression )(); if ( result === null ) { return null; } - return [ "CONCAT" ].concat( result ); + return [ 'CONCAT' ].concat( result ); } // everything above this point is supposed to be stateless/static, but // I am deferring the work of turning it into prototypes & objects. It's quite fast enough // finally let's do some actual work... - var result = start(); + + // If you add another possible rootExpression, you must update the astCache key scheme. + result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression ); /* * For success, the p must have gotten to the end of the input * and returned a non-null. * n.b. This is part of language infrastructure, so we do not throw an internationalizable message. */ - if (result === null || pos !== input.length) { - throw new Error( "Parse error at position " + pos.toString() + " in input: " + input ); + if ( result === null || pos !== input.length ) { + throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input ); } return result; } @@ -491,18 +608,20 @@ * @return {Mixed} single-string node or array of nodes suitable for jQuery appending */ this.emit = function ( node, replacements ) { - var ret = null; - var jmsg = this; + var ret, subnodes, operation, + jmsg = this; switch ( typeof node ) { case 'string': case 'number': ret = node; break; - case 'object': // node is an array of nodes - var subnodes = $.map( node.slice( 1 ), function ( n ) { + // typeof returns object for arrays + case 'object': + // node is an array of nodes + subnodes = $.map( node.slice( 1 ), function ( n ) { return jmsg.emit( n, replacements ); } ); - var operation = node[0].toLowerCase(); + operation = node[0].toLowerCase(); if ( typeof jmsg[operation] === 'function' ) { ret = jmsg[ operation ]( subnodes, replacements ); } else { @@ -543,8 +662,9 @@ $span.append( childNode ); } ); } else { - // strings, integers, anything else - $span.append( node ); + // Let jQuery append nodes, arrays of nodes and jQuery objects + // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings) + $span.append( $.type( node ) === 'object' ? node : document.createTextNode( node ) ); } } ); return $span; @@ -555,7 +675,7 @@ * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ]. * if the specified parameter is not found return the same string * (e.g. "$99" -> parameter 98 -> not found -> return "$99" ) - * TODO throw error if nodes.length > 1 ? + * TODO: Throw error if nodes.length > 1 ? * @param {Array} of one element, integer, n >= 0 * @return {String} replacement */ @@ -563,13 +683,7 @@ var index = parseInt( nodes[0], 10 ); if ( index < replacements.length ) { - if ( typeof arg === 'string' ) { - // replacement is a string, escape it - return mw.html.escape( replacements[index] ); - } else { - // replacement is no string, don't touch! - return replacements[index]; - } + return replacements[index]; } else { // index not found, fallback to displaying variable return '$' + ( index + 1 ); @@ -578,10 +692,41 @@ /** * Transform wiki-link - * TODO unimplemented + * + * TODO: + * It only handles basic cases, either no pipe, or a pipe with an explicit + * anchor. + * + * It does not attempt to handle features like the pipe trick. + * However, the pipe trick should usually not be present in wikitext retrieved + * from the server, since the replacement is done at save time. + * It may, though, if the wikitext appears in extension-controlled content. + * + * @param nodes */ wlink: function ( nodes ) { - return 'unimplemented'; + var page, anchor, url; + + page = nodes[0]; + url = mw.util.wikiGetlink( page ); + + // [[Some Page]] or [[Namespace:Some Page]] + if ( nodes.length === 1 ) { + anchor = page; + } + + /* + * [[Some Page|anchor text]] or + * [[Namespace:Some Page|anchor] + */ + else { + anchor = nodes[1]; + } + + return $( '
' ).attr( { + title: page, + href: url + } ).text( anchor ); }, /** @@ -594,9 +739,9 @@ * @return {jQuery} */ link: function ( nodes ) { - var arg = nodes[0]; - var contents = nodes[1]; - var $el; + var $el, + arg = nodes[0], + contents = nodes[1]; if ( arg instanceof jQuery ) { $el = arg; } else { @@ -639,25 +784,32 @@ * @return {String} selected pluralized form according to current language */ plural: function ( nodes ) { - var count = parseFloat( this.language.convertNumber( nodes[0], true ) ); - var forms = nodes.slice(1); + var forms, count; + count = parseFloat( this.language.convertNumber( nodes[0], true ) ); + forms = nodes.slice(1); return forms.length ? this.language.convertPlural( count, forms ) : ''; }, /** - * Transform parsed structure into gender - * Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}. - * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] + * Transform parsed structure according to gender. + * Usage {{gender:[ gender | mw.user object ] | masculine form|feminine form|neutral form}}. + * The first node is either a string, which can be "male" or "female", + * or a User object (not a username). + * + * @param {Array} of nodes, [ {String|mw.User}, {String}, {String}, {String} ] * @return {String} selected gender form according to current language */ gender: function ( nodes ) { - var gender; - if ( nodes[0] && nodes[0].options instanceof mw.Map ){ + var gender, forms; + + if ( nodes[0] && nodes[0].options instanceof mw.Map ) { gender = nodes[0].options.get( 'gender' ); } else { gender = nodes[0]; } - var forms = nodes.slice(1); + + forms = nodes.slice( 1 ); + return this.language.gender( gender, forms ); }, @@ -668,9 +820,33 @@ * @return {String} selected grammatical form according to current language */ grammar: function ( nodes ) { - var form = nodes[0]; - var word = nodes[1]; + var form = nodes[0], + word = nodes[1]; return word && form && this.language.convertGrammar( word, form ); + }, + + /** + * Tranform parsed structure into a int: (interface language) message include + * Invoked by putting {{int:othermessage}} into a message + * @param {Array} of nodes + * @return {string} Other message + */ + int: function ( nodes ) { + return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() ); + }, + + /** + * Takes an unformatted number (arab, no group separators and . as decimal separator) + * and outputs it in the localized digit script and formatted with decimal + * separator, according to the current language + * @param {Array} of nodes + * @return {Number|String} formatted number + */ + formatnum: function ( nodes ) { + var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false, + number = nodes[0]; + + return this.language.convertNumber( number, isInteger ); } }; // Deprecated! don't rely on gM existing. @@ -681,17 +857,24 @@ $.fn.msg = mw.jqueryMsg.getPlugin(); // Replace the default message parser with jqueryMsg - var oldParser = mw.Message.prototype.parser; + oldParser = mw.Message.prototype.parser; mw.Message.prototype.parser = function () { + var messageFunction; + // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe? // Caching is somewhat problematic, because we do need different message functions for different maps, so // we'd have to cache the parser as a member of this.map, which sounds a bit ugly. // Do not use mw.jqueryMsg unless required - if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) { + if ( this.format === 'plain' || !/\{\{|\[/.test(this.map.get( this.key ) ) ) { // Fall back to mw.msg's simple parser return oldParser.apply( this ); } - var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } ); + + messageFunction = mw.jqueryMsg.getMessageFunction( { + 'messages': this.map, + // For format 'escaped', escaping part is handled by mediawiki.js + 'format': this.format + } ); return messageFunction( this.key, this.parameters ); }; diff --git a/resources/mediawiki/mediawiki.jqueryMsg.peg b/resources/mediawiki/mediawiki.jqueryMsg.peg index e059ed1d..7879d6fa 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.peg +++ b/resources/mediawiki/mediawiki.jqueryMsg.peg @@ -37,6 +37,7 @@ templateParam templateName = tn:[A-Za-z_]+ { return tn.join('').toUpperCase() } +/* TODO: Update to reflect separate piped and unpiped handling */ link = "[[" w:expression "]]" { return [ 'WLINK', w ]; } diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 19112aed..ca987543 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -1,9 +1,9 @@ /* * Core MediaWiki JavaScript Library */ -/*global mw:true */ + var mw = ( function ( $, undefined ) { - "use strict"; + 'use strict'; /* Private Members */ @@ -13,14 +13,13 @@ var mw = ( function ( $, undefined ) { /* Object constructors */ /** - * Map - * * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. + * @class mw.Map * - * @param global boolean Whether to store the values in the global window + * @constructor + * @param {boolean} global Whether to store the values in the global window * object or a exclusively in the object property 'values'. - * @return Map */ function Map( global ) { this.values = global === true ? window : {}; @@ -39,26 +38,26 @@ var mw = ( function ( $, undefined ) { * 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 Values as a string or object, null if invalid/inexistant. + * @return {string|Object|null} Values as a string or object, null if invalid/inexistant. */ 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 += 1 ) { + for ( i = 0; i < selection.length; i++ ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; } if ( typeof selection === 'string' ) { - if ( this.values[selection] === undefined ) { - if ( fallback !== undefined ) { - return fallback; - } - return null; + if ( !hasOwn.call( this.values, selection ) ) { + return fallback; } return this.values[selection]; } @@ -87,7 +86,7 @@ var mw = ( function ( $, undefined ) { } return true; } - if ( typeof selection === 'string' && value !== undefined ) { + if ( typeof selection === 'string' && arguments.length > 1 ) { this.values[selection] = value; return true; } @@ -98,36 +97,35 @@ var mw = ( function ( $, undefined ) { * Checks if one or multiple keys exist. * * @param selection {mixed} String key or array of keys to check - * @return {Boolean} Existence of key(s) + * @return {boolean} Existence of key(s) */ exists: function ( selection ) { var s; if ( $.isArray( selection ) ) { - for ( s = 0; s < selection.length; s += 1 ) { - if ( this.values[selection[s]] === undefined ) { + for ( s = 0; s < selection.length; s++ ) { + if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) { return false; } } return true; } - return this.values[selection] !== undefined; + return typeof selection === 'string' && hasOwn.call( this.values, selection ); } }; /** - * Message - * * Object constructor for messages, * similar to the Message class in MediaWiki PHP. + * @class mw.Message * - * @param map Map Instance of mw.Map - * @param key String - * @param parameters Array - * @return Message + * @constructor + * @param {mw.Map} map Message storage + * @param {string} key + * @param {Array} [parameters] */ function Message( map, key, parameters ) { - this.format = 'plain'; + this.format = 'text'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); @@ -136,9 +134,13 @@ var mw = ( function ( $, undefined ) { Message.prototype = { /** - * Simple message parser, does $N replacement and nothing else. + * Simple message parser, does $N replacement, HTML-escaping (only for + * 'escaped' format), and nothing else. + * * This may be overridden to provide a more complex message parser. * + * The primary override is in mediawiki.jqueryMsg. + * * This function will not be called for nonexistent messages. */ parser: function () { @@ -152,8 +154,8 @@ var mw = ( function ( $, undefined ) { /** * Appends (does not replace) parameters for replacement to the .parameters property. * - * @param parameters Array - * @return Message + * @param {Array} parameters + * @chainable */ params: function ( parameters ) { var i; @@ -166,25 +168,21 @@ var mw = ( function ( $, undefined ) { /** * Converts message object to it's string form based on the state of format. * - * @return string Message as a string in the current form or if key does not exist. + * @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 !== 'plain' ) { - // format 'escape' and 'parse' need to have the brackets and key html escaped + 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' ) { - // @todo FIXME: Although not applicable to core Message, - // Plugins like jQueryMsg should be able to distinguish - // between 'plain' (only variable replacement and plural/gender) - // and actually parsing wikitext to HTML. + if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { text = this.parser(); } @@ -193,15 +191,16 @@ var mw = ( function ( $, undefined ) { text = mw.html.escape( text ); } - if ( this.format === 'parse' ) { - text = this.parser(); - } - return text; }, /** - * Changes format to parse and converts message to string + * Changes format to 'parse' and converts 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 */ @@ -211,7 +210,10 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes format to plain and converts message to string + * Changes format to 'plain' and converts message to string + * + * This substitutes parameters, but otherwise does not change the + * message text. * * @return {string} String form of plain message */ @@ -221,7 +223,23 @@ var mw = ( function ( $, undefined ) { }, /** - * Changes the format to html escaped and converts message to string + * Changes format to 'text' and converts message to string + * + * If jqueryMsg is loaded, {{-transformation is done where supported + * (such as {{plural:}}, {{gender:}}, {{int:}}). + * + * Otherwise, it is equivalent to plain. + */ + text: function () { + this.format = 'text'; + return this.toString(); + }, + + /** + * Changes the format to 'escaped' and converts message to string + * + * This is equivalent to using the 'text' format (see text method), then + * HTML-escaping the output. * * @return {string} String form of html escaped message */ @@ -233,13 +251,19 @@ var mw = ( function ( $, undefined ) { /** * Checks if message exists * - * @return {string} String form of parsed message + * @see mw.Map#exists + * @return {boolean} */ exists: function () { return this.map.exists( this.key ); } }; + /** + * @class mw + * @alternateClassName mediaWiki + * @singleton + */ return { /* Public Members */ @@ -249,77 +273,72 @@ var mw = ( function ( $, undefined ) { */ log: function () { }, - /** - * @var constructor Make the Map constructor publicly available. - */ + // Make the Map constructor publicly available. Map: Map, - /** - * @var constructor Make the Message constructor publicly available. - */ + // Make the Message constructor publicly available. Message: Message, /** * List of configuration values * * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). - * If $wgLegacyJavaScriptGlobals is true, this Map will have its values + * If `$wgLegacyJavaScriptGlobals` is true, this Map will have its values * in the global window object. + * @property */ config: null, /** - * @var object - * * Empty object that plugins can be installed in. + * @property */ libs: {}, /* Extension points */ + /** + * @property + */ legacy: {}, /** * Localization system + * @property {mw.Map} */ messages: new Map(), /* Public Methods */ /** - * Gets a message object, similar to wfMessage() + * Gets a message object, similar to wfMessage(). * - * @param key string Key of message to get - * @param parameter_1 mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return Message + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {mw.Message} */ - message: function ( key, parameter_1 /* [, parameter_2] */ ) { - var parameters; - // Support variadic arguments - if ( parameter_1 !== undefined ) { - parameters = slice.call( arguments ); - parameters.shift(); - } else { - parameters = []; - } + message: function ( key ) { + // Variadic arguments + var parameters = slice.call( arguments, 1 ); return new Message( mw.messages, key, parameters ); }, /** * Gets a message string, similar to wfMessage() * - * @param key string Key of message to get - * @param parameters mixed First argument in a list of variadic arguments, - * each a parameter for $N replacement in messages. - * @return String. + * @see mw.Message#toString + * @param {string} key Key of message to get + * @param {Mixed...} parameters Parameters for the $N replacements in messages. + * @return {string} */ - msg: function ( /* key, parameter_1, parameter_2, .. */ ) { + msg: function ( /* key, parameters... */ ) { return mw.message.apply( mw.message, arguments ).toString(); }, /** * Client-side module loader which integrates with the MediaWiki ResourceLoader + * @class mw.loader + * @singleton */ loader: ( function () { @@ -338,29 +357,32 @@ var mw = ( function ( $, undefined ) { * mw.loader.implement. * * Format: - * { - * 'moduleName': { - * 'version': ############## (unix timestamp), - * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} - * 'group': 'somegroup', (or) null, - * 'source': 'local', 'someforeignwiki', (or) null - * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing' - * 'script': ..., - * 'style': ..., - * 'messages': { 'key': 'value' }, - * } - * } + * { + * 'moduleName': { + * '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' + * 'script': ..., + * 'style': ..., + * 'messages': { 'key': 'value' }, + * } + * } + * + * @property + * @private */ var registry = {}, - /** - * Mapping of sources, keyed by source-id, values are objects. - * Format: - * { - * 'sourceId': { - * 'loadScript': 'http://foo.bar/w/load.php' - * } - * } - */ + // + // Mapping of sources, keyed by source-id, values are objects. + // Format: + // { + // 'sourceId': { + // 'loadScript': 'http://foo.bar/w/load.php' + // } + // } + // sources = {}, // List of modules which will be loaded as when ready batch = [], @@ -369,7 +391,11 @@ var mw = ( function ( $, undefined ) { // 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; + $marker = null, + // Buffer for addEmbeddedCSS. + cssBuffer = '', + // Callbacks for addEmbeddedCSS. + cssCallbacks = $.Callbacks(); /* Private methods */ @@ -392,10 +418,11 @@ var mw = ( function ( $, undefined ) { /** * Create a new style tag and add it to the DOM. * - * @param text String: CSS text - * @param nextnode mixed: [optional] An Element or jQuery object for an element where - * the style tag should be inserted before. Otherwise appended to the . - * @return HTMLStyleElement + * @private + * @param {string} text CSS text + * @param {Mixed} [nextnode] An Element or jQuery object for an element where + * the style tag should be inserted before. Otherwise appended to the ``. + * @return {HTMLElement} Node reference to the created `