From 63601400e476c6cf43d985f3e7b9864681695ed4 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 18 Jan 2013 16:46:04 +0100 Subject: Update to MediaWiki 1.20.2 this update includes: * adjusted Arch Linux skin * updated FluxBBAuthPlugin * patch for https://bugzilla.wikimedia.org/show_bug.cgi?id=44024 --- resources/mediawiki/mediawiki.Title.js | 104 ++-- resources/mediawiki/mediawiki.Uri.js | 103 ++-- resources/mediawiki/mediawiki.debug.css | 1 - resources/mediawiki/mediawiki.debug.js | 68 ++- resources/mediawiki/mediawiki.feedback.js | 182 +++--- resources/mediawiki/mediawiki.htmlform.js | 20 +- resources/mediawiki/mediawiki.jqueryMsg.js | 411 +++++++------- resources/mediawiki/mediawiki.jqueryMsg.peg | 4 + resources/mediawiki/mediawiki.js | 751 ++++++++++++++++--------- resources/mediawiki/mediawiki.log.js | 11 +- resources/mediawiki/mediawiki.notification.css | 26 + resources/mediawiki/mediawiki.notification.js | 480 ++++++++++++++++ resources/mediawiki/mediawiki.notify.js | 20 + resources/mediawiki/mediawiki.searchSuggest.js | 166 ++++++ resources/mediawiki/mediawiki.user.js | 128 ++++- resources/mediawiki/mediawiki.util.js | 171 +++--- 16 files changed, 1850 insertions(+), 796 deletions(-) create mode 100644 resources/mediawiki/mediawiki.notification.css create mode 100644 resources/mediawiki/mediawiki.notification.js create mode 100644 resources/mediawiki/mediawiki.notify.js create mode 100644 resources/mediawiki/mediawiki.searchSuggest.js (limited to 'resources/mediawiki') diff --git a/resources/mediawiki/mediawiki.Title.js b/resources/mediawiki/mediawiki.Title.js index 8d7996cb..33cca585 100644 --- a/resources/mediawiki/mediawiki.Title.js +++ b/resources/mediawiki/mediawiki.Title.js @@ -7,7 +7,7 @@ * * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces), mw.util.wikiGetlink */ -( function( $ ) { +( function ( mw, $ ) { /* Local space */ @@ -20,19 +20,25 @@ * @param namespace {Number} (optional) Namespace id. If given, title will be taken as-is. * @return {Title} this */ -var Title = function( title, namespace ) { - this._ns = 0; // integer namespace id - this._name = null; // name in canonical 'database' form - this._ext = null; // extension + function Title( title, namespace ) { + this.ns = 0; // integer namespace id + this.name = null; // name in canonical 'database' form + this.ext = null; // extension if ( arguments.length === 2 ) { setNameAndExtension( this, title ); - this._ns = fixNsId( namespace ); + this.ns = fixNsId( namespace ); } else if ( arguments.length === 1 ) { setAll( this, title ); } return this; - }, + } + +var + /** + * Public methods (defined later) + */ + fn, /** * Strip some illegal chars: control chars, colon, less than, greater than, @@ -41,7 +47,7 @@ var Title = function( title, namespace ) { * @param s {String} * @return {String} */ - clean = function( s ) { + clean = function ( s ) { if ( s !== undefined ) { return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '_' ); } @@ -63,14 +69,14 @@ var Title = function( title, namespace ) { /** * Sanitize name. */ - fixName = function( s ) { + fixName = function ( s ) { return clean( $.trim( s ) ); }, /** * Sanitize name. */ - fixExt = function( s ) { + fixExt = function ( s ) { return clean( s ); }, @@ -79,7 +85,7 @@ var Title = function( title, namespace ) { * @param id {Number} Namespace id. * @return {Number|Boolean} The id as-is or boolean false if invalid. */ - fixNsId = function( id ) { + fixNsId = function ( id ) { // wgFormattedNamespaces is an object of *string* key-vals (ie. arr["0"] not arr[0] ) var ns = mw.config.get( 'wgFormattedNamespaces' )[id.toString()]; @@ -98,9 +104,13 @@ var Title = function( title, namespace ) { * @param ns {String} Namespace name (case insensitive, leading/trailing space ignored). * @return {Number|Boolean} Namespace id or boolean false if unrecognized. */ - getNsIdByName = function( ns ) { - // toLowerCase throws exception on null/undefined. Return early. - if ( ns == null ) { + getNsIdByName = function ( ns ) { + // Don't cast non-strings to strings, because null or undefined + // should not result in returning the id of a potential namespace + // called "Null:" (e.g. on nullwiki.example.org) + // Also, toLowerCase throws exception on null/undefined, because + // it is a String.prototype method. + if ( typeof ns !== 'string' ) { return false; } ns = clean( $.trim( ns.toLowerCase() ) ); // Normalize @@ -119,22 +129,22 @@ var Title = function( title, namespace ) { * @param raw {String} * @return {mw.Title} */ - setAll = function( title, s ) { + 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{1,5}))?$/ ), + var matches = s.match( /^(?:([^:]+):)?(.*?)(?:\.(\w+))?$/ ), ns_match = 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; - title._name = fixName( matches[2] ); + title.ns = ns_match; + title.name = fixName( matches[2] ); if ( typeof matches[3] === 'string' && matches[3] !== '' ) { - title._ext = fixExt( matches[3] ); + title.ext = fixExt( matches[3] ); } } else { // Consistency with MediaWiki PHP: Unknown namespace -> fallback to main namespace. - title._ns = 0; + title.ns = 0; setNameAndExtension( title, s ); } return title; @@ -147,16 +157,16 @@ var Title = function( title, namespace ) { * @param raw {String} * @return {mw.Title} */ - setNameAndExtension = function( title, raw ) { + setNameAndExtension = function ( title, raw ) { // In normal browsers the match-array contains null/undefined if there's no match, // IE returns an empty string. - var matches = raw.match( /^(?:)?(.*?)(?:\.(\w{1,5}))?$/ ); + var matches = raw.match( /^(?:)?(.*?)(?:\.(\w+))?$/ ); // Title must be a non-empty string. if ( typeof matches[1] === 'string' && matches[1] !== '' ) { - title._name = fixName( matches[1] ); + title.name = fixName( matches[1] ); if ( typeof matches[2] === 'string' && matches[2] !== '' ) { - title._ext = fixExt( matches[2] ); + title.ext = fixExt( matches[2] ); } } else { throw new Error( 'mw.Title: Could not parse title "' + raw + '"' ); @@ -172,7 +182,7 @@ var Title = function( title, namespace ) { * @param title {mixed} 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 ) { + Title.exists = function ( title ) { var type = $.type( title ), obj = Title.exist.pages, match; if ( type === 'string' ) { match = obj[title]; @@ -203,7 +213,7 @@ var Title = function( title, namespace ) { * @param state {Boolean} (optional) State of the given titles. Defaults to true. * @return {Boolean} */ - set: function( titles, state ) { + set: function ( titles, state ) { titles = $.isArray( titles ) ? titles : [titles]; state = state === undefined ? true : !!state; var pages = this.pages, i, len = titles.length; @@ -216,15 +226,15 @@ var Title = function( title, namespace ) { /* Public methods */ - var fn = { + fn = { constructor: Title, /** * Get the namespace number. * @return {Number} */ - getNamespaceId: function(){ - return this._ns; + getNamespaceId: function (){ + return this.ns; }, /** @@ -232,19 +242,19 @@ var Title = function( title, namespace ) { * In NS_MAIN this is '', otherwise namespace name plus ':' * @return {String} */ - getNamespacePrefix: function(){ - return mw.config.get( 'wgFormattedNamespaces' )[this._ns].replace( / /g, '_' ) + (this._ns === 0 ? '' : ':'); + getNamespacePrefix: function (){ + return mw.config.get( 'wgFormattedNamespaces' )[this.ns].replace( / /g, '_' ) + (this.ns === 0 ? '' : ':'); }, /** * The name, like "Foo_bar" * @return {String} */ - getName: function() { - if ( $.inArray( this._ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { - return this._name; + getName: function () { + if ( $.inArray( this.ns, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { + return this.name; } else { - return $.ucFirst( this._name ); + return $.ucFirst( this.name ); } }, @@ -252,7 +262,7 @@ var Title = function( title, namespace ) { * The name, like "Foo bar" * @return {String} */ - getNameText: function() { + getNameText: function () { return text( this.getName() ); }, @@ -260,7 +270,7 @@ var Title = function( title, namespace ) { * Get full name in prefixed DB form, like File:Foo_bar.jpg, * most useful for API calls, anything that must identify the "title". */ - getPrefixedDb: function() { + getPrefixedDb: function () { return this.getNamespacePrefix() + this.getMain(); }, @@ -268,7 +278,7 @@ var Title = function( title, namespace ) { * Get full name in text form, like "File:Foo bar.jpg". * @return {String} */ - getPrefixedText: function() { + getPrefixedText: function () { return text( this.getPrefixedDb() ); }, @@ -276,7 +286,7 @@ var Title = function( title, namespace ) { * The main title (without namespace), like "Foo_bar.jpg" * @return {String} */ - getMain: function() { + getMain: function () { return this.getName() + this.getDotExtension(); }, @@ -284,7 +294,7 @@ var Title = function( title, namespace ) { * The "text" form, like "Foo bar.jpg" * @return {String} */ - getMainText: function() { + getMainText: function () { return text( this.getMain() ); }, @@ -292,23 +302,23 @@ var Title = function( title, namespace ) { * Get the extension (returns null if there was none) * @return {String|null} extension */ - getExtension: function() { - return this._ext; + getExtension: function () { + return this.ext; }, /** * Convenience method: return string like ".jpg", or "" if no extension * @return {String} */ - getDotExtension: function() { - return this._ext === null ? '' : '.' + this._ext; + getDotExtension: function () { + return this.ext === null ? '' : '.' + this.ext; }, /** * Return the URL to this title * @return {String} */ - getUrl: function() { + getUrl: function () { return mw.util.wikiGetlink( this.toString() ); }, @@ -316,7 +326,7 @@ var Title = function( title, namespace ) { * Whether this title exists on the wiki. * @return {mixed} Boolean true/false if the information is available. Otherwise null. */ - exists: function() { + exists: function () { return Title.exists( this ); } }; @@ -331,4 +341,4 @@ var Title = function( title, namespace ) { // Expose mw.Title = Title; -})(jQuery); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js index 26fdfa9e..bd12b214 100644 --- a/resources/mediawiki/mediawiki.Uri.js +++ b/resources/mediawiki/mediawiki.Uri.js @@ -56,7 +56,7 @@ * */ -( function( $, mw ) { +( function ( mw, $ ) { /** * Function that's useful when constructing the URI string -- we frequently encounter the pattern of @@ -70,9 +70,8 @@ function cat( pre, val, post, raw ) { if ( val === undefined || val === null || val === '' ) { return ''; - } else { - return pre + ( raw ? val : mw.Uri.encode( val ) ) + post; } + return pre + ( raw ? val : mw.Uri.encode( val ) ) + post; } // Regular expressions to parse many common URIs. @@ -98,13 +97,16 @@ * We use a factory to inject a document location, for relative URLs, including protocol-relative URLs. * so the library is still testable & purely functional. */ - mw.UriRelative = function( documentLocation ) { + mw.UriRelative = function ( documentLocation ) { + var defaultUri; /** * 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). * 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 @@ -117,25 +119,48 @@ overrideKeys: false }, options ); - if ( uri !== undefined && uri !== null || uri !== '' ) { + if ( uri !== undefined && uri !== null && uri !== '' ) { if ( typeof uri === 'string' ) { - this._parse( uri, options ); + this.parse( uri, options ); } else if ( typeof uri === 'object' ) { - var _this = this; - $.each( properties, function( i, property ) { - _this[property] = uri[property]; - } ); - if ( this.query === undefined ) { + // Copy data over from existing URI object + for ( var prop in uri ) { + // Only copy direct properties, not inherited ones + if ( uri.hasOwnProperty( prop ) ) { + // Deep copy object properties + if ( $.isArray( uri[prop] ) || $.isPlainObject( uri[prop] ) ) { + this[prop] = $.extend( true, {}, uri[prop] ); + } else { + this[prop] = uri[prop]; + } + } + } + if ( !this.query ) { this.query = {}; } } + } else { + // If we didn't get a URI in the constructor, use the default one. + return defaultUri.clone(); } // protocol-relative URLs if ( !this.protocol ) { - this.protocol = defaultProtocol; + this.protocol = defaultUri.protocol; + } + // No host given: + if ( !this.host ) { + this.host = defaultUri.host; + // port ? + if ( !this.port ) { + this.port = defaultUri.port; + } + } + 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. + throw new Error( 'Bad constructor arguments' ); } - if ( !( this.protocol && this.host && this.path ) ) { throw new Error( 'Bad constructor arguments' ); } @@ -147,7 +172,7 @@ * @param {String} string * @return {String} encoded for URI */ - Uri.encode = function( s ) { + Uri.encode = function ( s ) { return encodeURIComponent( s ) .replace( /!/g, '%21').replace( /'/g, '%27').replace( /\(/g, '%28') .replace( /\)/g, '%29').replace( /\*/g, '%2A') @@ -159,7 +184,7 @@ * @param {String} string encoded for URI * @return {String} decoded string */ - Uri.decode = function( s ) { + Uri.decode = function ( s ) { return decodeURIComponent( s.replace( /\+/g, '%20' ) ); }; @@ -171,23 +196,25 @@ * @param {Object} options * @return {Boolean} success */ - _parse: function( str, options ) { - var matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str ); - var uri = this; - $.each( properties, function( i, property ) { + parse: function ( str, options ) { + var q, + uri = this, + matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str ); + $.each( properties, function ( i, property ) { uri[ property ] = matches[ i+1 ]; } ); // uri.query starts out as the query string; we will parse it into key-val pairs then make // that object the "query" property. // we overwrite query in uri way to make cloning easier, it can use the same list of properties. - var q = {}; + q = {}; // using replace to iterate over a string if ( uri.query ) { uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { + var k, v; if ( $1 ) { - var k = Uri.decode( $1 ); - var v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 ); + k = Uri.decode( $1 ); + v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 ); // If overrideKeys, always (re)set top level value. // If not overrideKeys but this key wasn't set before, then we set it as well. @@ -215,7 +242,7 @@ * Returns user and password portion of a URI. * @return {String} */ - getUserInfo: function() { + getUserInfo: function () { return cat( '', this.user, cat( ':', this.password, '' ) ); }, @@ -223,7 +250,7 @@ * Gets host and port portion of a URI. * @return {String} */ - getHostPort: function() { + getHostPort: function () { return this.host + cat( ':', this.port, '' ); }, @@ -232,7 +259,7 @@ * In most real-world URLs, this is simply the hostname, but it is more general. * @return {String} */ - getAuthority: function() { + getAuthority: function () { return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); }, @@ -241,12 +268,12 @@ * Does not preserve the order of arguments passed into the URI. Does handle escaping. * @return {String} */ - getQueryString: function() { + getQueryString: function () { var args = []; - $.each( this.query, function( key, val ) { - var k = Uri.encode( key ); - var vals = val === null ? [ null ] : $.makeArray( val ); - $.each( vals, function( i, v ) { + $.each( this.query, function ( key, val ) { + var k = Uri.encode( key ), + vals = $.isArray( val ) ? val : [ val ]; + $.each( vals, function ( i, v ) { args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) ); } ); } ); @@ -257,7 +284,7 @@ * Returns everything after the authority section of the URI * @return {String} */ - getRelativePath: function() { + getRelativePath: function () { return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); }, @@ -265,7 +292,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 */ - toString: function() { + toString: function () { return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); }, @@ -273,7 +300,7 @@ * Clone this URI * @return {Object} new URI object with same properties */ - clone: function() { + clone: function () { return new Uri( this ); }, @@ -282,20 +309,20 @@ * @param {Object} query parameters in key-val form to override or add * @return {Object} this URI object */ - extend: function( parameters ) { + extend: function ( parameters ) { $.extend( this.query, parameters ); return this; } }; - var defaultProtocol = ( new Uri( documentLocation ) ).protocol; + defaultUri = new Uri( documentLocation ); - return Uri; + return Uri; }; // if we are running in a browser, inject the current document location, for relative URLs - if ( document && document.location && document.location.href ) { + if ( document && document.location && document.location.href ) { mw.Uri = mw.UriRelative( document.location.href ); } -} )( jQuery, mediaWiki ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.debug.css b/resources/mediawiki/mediawiki.debug.css index 923d4a47..149e1bff 100644 --- a/resources/mediawiki/mediawiki.debug.css +++ b/resources/mediawiki/mediawiki.debug.css @@ -6,7 +6,6 @@ } .mw-debug pre { - font-family: Monaco, "Consolas", "Lucida Console", "Courier New", monospace; font-size: 11px; padding: 0; margin: 0; diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js index a2bfbcbe..1ad1a623 100644 --- a/resources/mediawiki/mediawiki.debug.js +++ b/resources/mediawiki/mediawiki.debug.js @@ -5,12 +5,13 @@ * @since 1.19 */ -( function ( $, mw, undefined ) { -"use strict"; +( function ( mw, $ ) { + 'use strict'; - var hovzer = $.getFootHovzer(); + var debug, + hovzer = $.getFootHovzer(); - var debug = mw.Debug = { + debug = mw.Debug = { /** * Toolbar container element * @@ -93,7 +94,7 @@ * Constructs the HTML for the debugging toolbar */ buildHtml: function () { - var $container, $bits, panes, id; + var $container, $bits, panes, id, gitInfo; $container = $( '
' ); @@ -106,9 +107,9 @@ * @return {jQuery} */ function bitDiv( id ) { - return $( '
' ).attr({ + return $( '
' ).prop({ id: 'mw-debug-' + id, - 'class': 'mw-debug-bit' + className: 'mw-debug-bit' }) .appendTo( $bits ); } @@ -122,8 +123,8 @@ */ function paneLabel( id, text ) { return $( '' ) - .attr({ - 'class': 'mw-debug-panelabel', + .prop({ + className: 'mw-debug-panelabel', href: '#mw-debug-pane-' + id }) .text( text ); @@ -138,12 +139,12 @@ * @return {jQuery} */ function paneTriggerBitDiv( id, text, count ) { - if( count ) { + if ( count ) { text = text + ' (' + count + ')'; } - return $( '
' ).attr({ + return $( '
' ).prop({ id: 'mw-debug-' + id, - 'class': 'mw-debug-bit mw-debug-panelink' + className: 'mw-debug-bit mw-debug-panelink' }) .append( paneLabel( id, text ) ) .appendTo( $bits ); @@ -159,9 +160,24 @@ paneTriggerBitDiv( 'includes', 'PHP includes', this.data.includes.length ); + gitInfo = ''; + if ( this.data.gitRevision !== false ) { + gitInfo = '(' + this.data.gitRevision.substring( 0, 7 ) + ')'; + if ( this.data.gitViewUrl !== false ) { + gitInfo = $( '' ) + .attr( 'href', this.data.gitViewUrl ) + .text( gitInfo ); + } + } + bitDiv( 'mwversion' ) - .append( $( '' ).text( 'MediaWiki' ) ) - .append( ': ' + this.data.mwVersion ); + .append( $( 'MediaWiki' ) ) + .append( document.createTextNode( ': ' + this.data.mwVersion + ' ' ) ) + .append( gitInfo ); + + if ( this.data.gitBranch !== false ) { + bitDiv( 'gitbranch' ).text( 'Git branch: ' + this.data.gitBranch ); + } bitDiv( 'phpversion' ) .append( $( '' ).text( 'PHP' ) ) @@ -191,8 +207,8 @@ } $( '
' ) - .attr({ - 'class': 'mw-debug-pane', + .prop({ + className: 'mw-debug-pane', id: 'mw-debug-pane-' + id }) .append( panes[id] ) @@ -210,9 +226,9 @@ $table = $( '' ); - $('').css( 'width', /*padding=*/20 + ( 10*/*fontSize*/11 ) ).appendTo( $table ); - $('').appendTo( $table ); - $('').css( 'width', 350 ).appendTo( $table ); + $( '' ).css( 'width', /* padding = */ 20 + ( 10 * /* fontSize = */ 11 ) ).appendTo( $table ); + $( '' ).appendTo( $table ); + $( '' ).css( 'width', 350 ).appendTo( $table ); entryTypeText = function( entryType ) { @@ -235,7 +251,7 @@ $( '' ) .append( $( '' ) - .append( $('').css( 'width', '4em' ) ) - .append( $('') ) - .append( $('').css( 'width', '8em' ) ) - .append( $('').css( 'width', '18em' ) ) + .append( $( '' ).css( 'width', '4em' ) ) + .append( $( '' ) ) + .append( $( '' ).css( 'width', '8em' ) ) + .append( $( '' ).css( 'width', '18em' ) ) .appendTo( $table ); for ( i = 0, length = this.data.queries.length; i < length; i += 1 ) { @@ -285,7 +301,7 @@ for ( i = 0, length = this.data.debugLog.length; i < length; i += 1 ) { line = this.data.debugLog[i]; $( '
  • ' ) - .html( mw.html.escape( line ).replace( /\n/g, "
    \n" ) ) + .html( mw.html.escape( line ).replace( /\n/g, '
    \n' ) ) .appendTo( $list ); } @@ -348,4 +364,4 @@ } }; -} )( jQuery, mediaWiki ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js index 9a4a7298..634d02b1 100644 --- a/resources/mediawiki/mediawiki.feedback.js +++ b/resources/mediawiki/mediawiki.feedback.js @@ -22,22 +22,22 @@ * Minimal example in how to use it: * * var feedback = new mw.Feedback(); - * $( '#myButton' ).click( function() { feedback.launch(); } ); + * $( '#myButton' ).click( function () { feedback.launch(); } ); * * You can also launch the feedback form with a prefilled subject and body. * See the docs for the launch() method. */ -( function( mw, $, undefined ) { +( function ( mw, $ ) { /** * Thingy for collecting user feedback on a wiki page * @param {Array} options -- optional, all properties optional. - * api: {mw.Api} if omitted, will just create a standard API - * title: {mw.Title} the title of the page where you collect feedback. Defaults to "Feedback". - * dialogTitleMessageKey: {String} message key for the title of the dialog box - * bugsLink: {mw.Uri|String} url where bugs can be posted - * bugsListLink: {mw.Uri|String} url where bugs can be listed + * api: {mw.Api} if omitted, will just create a standard API + * title: {mw.Title} the title of the page where you collect feedback. Defaults to "Feedback". + * dialogTitleMessageKey: {String} message key for the title of the dialog box + * bugsLink: {mw.Uri|String} url where bugs can be posted + * bugsListLink: {mw.Uri|String} url where bugs can be listed */ - mw.Feedback = function( options ) { + mw.Feedback = function ( options ) { if ( options === undefined ) { options = {}; } @@ -67,64 +67,69 @@ }; mw.Feedback.prototype = { - setup: function() { - var _this = this; + setup: function () { + var fb = this; - var $feedbackPageLink = $( '' ) - .attr( { 'href': _this.title.getUrl(), 'target': '_blank' } ) + var $feedbackPageLink = $( '' ) + .attr( { 'href': fb.title.getUrl(), 'target': '_blank' } ) .css( { 'white-space': 'nowrap' } ); - var $bugNoteLink = $( '' ).attr( { 'href': '#' } ).click( function() { _this.displayBugs(); } ); + var $bugNoteLink = $( '' ).attr( { 'href': '#' } ).click( function () { + fb.displayBugs(); + } ); - var $bugsListLink = $( '' ).attr( { 'href': _this.bugsListLink, 'target': '_blank' } ); + var $bugsListLink = $( '' ).attr( { 'href': fb.bugsListLink, 'target': '_blank' } ); + // TODO: Use a stylesheet instead of these inline styles this.$dialog = - $( '
    ' ).append( + $( '
    ' ).append( $( '' ).append( - $( '' ).append( - $( '

    ' ).msg( + $( '' ).append( + $( '

    ' ).msg( 'feedback-bugornote', $bugNoteLink, - _this.title.getNameText(), + fb.title.getNameText(), $feedbackPageLink.clone() ) ), - $( '

    ' ).append( + $( '
    ' ).append( mw.msg( 'feedback-subject' ), - $( '
    ' ), - $( '' ) + $( '
    ' ), + $( '' ) ), - $( '
    ' ).append( + $( '
    ' ).append( mw.msg( 'feedback-message' ), - $( '
    ' ), - $( '' ) + $( '
    ' ), + $( '' ) ) ), $( '' ).append( $( '

    ' ).msg( 'feedback-bugcheck', $bugsListLink ) ), - $( '

    ' ).append( + $( '' ).append( mw.msg( 'feedback-adding' ), $( '
    ' ), $( '' ) ), - $( '' ).msg( - 'feedback-thanks', _this.title.getNameText(), $feedbackPageLink.clone() + $( '' ).msg( + 'feedback-thanks', fb.title.getNameText(), $feedbackPageLink.clone() ), - $( '' ).append( - $( '' ) + $( '' ).append( + $( '' ) ) ); // undo some damage from dialog css - this.$dialog.find( 'a' ).css( { 'color': '#0645ad' } ); + this.$dialog.find( 'a' ).css( { + color: '#0645ad' + } ); this.$dialog.dialog({ width: 500, autoOpen: false, title: mw.msg( this.dialogTitleMessageKey ), modal: true, - buttons: _this.buttons + buttons: fb.buttons }); this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get(0); @@ -132,98 +137,119 @@ }, - display: function( s ) { + display: function ( s ) { this.$dialog.dialog( { buttons:{} } ); // hide the buttons this.$dialog.find( '.feedback-mode' ).hide(); // hide everything this.$dialog.find( '.feedback-' + s ).show(); // show the desired div }, - displaySubmitting: function() { + displaySubmitting: function () { this.display( 'submitting' ); }, - displayBugs: function() { - var _this = this; + displayBugs: function () { + var fb = this; this.display( 'bugs' ); var bugsButtons = {}; - bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function() { window.open( _this.bugsLink, '_blank' ); }; - bugsButtons[ mw.msg( 'feedback-cancel' ) ] = function() { _this.cancel(); }; - this.$dialog.dialog( { buttons: bugsButtons } ); + bugsButtons[ mw.msg( 'feedback-bugnew' ) ] = function () { + window.open( fb.bugsLink, '_blank' ); + }; + bugsButtons[ mw.msg( 'feedback-cancel' ) ] = function () { + fb.cancel(); + }; + this.$dialog.dialog( { + buttons: bugsButtons + } ); }, - displayThanks: function() { - var _this = this; + displayThanks: function () { + var fb = this; this.display( 'thanks' ); var closeButton = {}; - closeButton[ mw.msg( 'feedback-close' ) ] = function() { _this.$dialog.dialog( 'close' ); }; - this.$dialog.dialog( { buttons: closeButton } ); + closeButton[ mw.msg( 'feedback-close' ) ] = function () { + fb.$dialog.dialog( 'close' ); + }; + this.$dialog.dialog( { + buttons: closeButton + } ); }, /** * Display the feedback form * @param {Object} optional prefilled contents for the feedback form. Object with properties: - * subject: {String} - * message: {String} + * subject: {String} + * message: {String} */ - displayForm: function( contents ) { - var _this = this; - this.subjectInput.value = (contents && contents.subject) ? contents.subject : ''; - this.messageInput.value = (contents && contents.message) ? contents.message : ''; + displayForm: function ( contents ) { + var fb = this; + 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() { _this.submit(); }; - formButtons[ mw.msg( 'feedback-cancel' ) ] = function() { _this.cancel(); }; + formButtons[ mw.msg( 'feedback-submit' ) ] = function () { + fb.submit(); + }; + formButtons[ mw.msg( 'feedback-cancel' ) ] = function () { + fb.cancel(); + }; this.$dialog.dialog( { buttons: formButtons } ); // put the buttons back }, - displayError: function( message ) { - var _this = this; + displayError: function ( message ) { + var fb = this; this.display( 'error' ); this.$dialog.find( '.feedback-error-msg' ).msg( message ); var closeButton = {}; - closeButton[ mw.msg( 'feedback-close' ) ] = function() { _this.$dialog.dialog( 'close' ); }; + closeButton[ mw.msg( 'feedback-close' ) ] = function () { + fb.$dialog.dialog( 'close' ); + }; this.$dialog.dialog( { buttons: closeButton } ); }, - cancel: function() { + cancel: function () { this.$dialog.dialog( 'close' ); }, - submit: function() { - var _this = this; - - // get the values to submit - var subject = this.subjectInput.value; - - var message = "User agent: " + navigator.userAgent + "\n\n" - + this.messageInput.value; - if ( message.indexOf( '~~~' ) == -1 ) { - message += " ~~~~"; - } - - this.displaySubmitting(); + submit: function () { + var subject, message, + fb = this; - var ok = function( result ) { + function ok( result ) { if ( result.edit !== undefined ) { if ( result.edit.result === 'Success' ) { - _this.displayThanks(); + fb.displayThanks(); } else { - _this.displayError( 'feedback-error1' ); // unknown API result + // unknown API result + fb.displayError( 'feedback-error1' ); } } else { - _this.displayError( 'feedback-error2' ); // edit failed + // edit failed + fb.displayError( 'feedback-error2' ); } - }; + } - var err = function( code, info ) { - _this.displayError( 'feedback-error3' ); // ajax request failed - }; + function err( code, info ) { + // ajax request failed + fb.displayError( 'feedback-error3' ); + } + + // Get the values to submit. + subject = this.subjectInput.value; + + // We used to include "mw.html.escape( navigator.userAgent )" but there are legal issues + // with posting this without their explicit consent + message = this.messageInput.value; + if ( message.indexOf( '~~~' ) === -1 ) { + message += ' ~~~~'; + } + + this.displaySubmitting(); this.api.newSection( this.title, subject, message, ok, err ); - }, // close submit button function + }, /** * Modify the display form, and then open it, focusing interface on the subject. @@ -231,7 +257,7 @@ * subject: {String} * message: {String} */ - launch: function( contents ) { + launch: function ( contents ) { this.displayForm( contents ); this.$dialog.dialog( 'open' ); this.subjectInput.focus(); @@ -239,4 +265,4 @@ }; -} )( window.mediaWiki, jQuery ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js index 17a02cf4..a4753b99 100644 --- a/resources/mediawiki/mediawiki.htmlform.js +++ b/resources/mediawiki/mediawiki.htmlform.js @@ -1,7 +1,7 @@ /** * Utility functions for jazzing up HTMLForm elements */ -( function( $ ) { +( function ( $ ) { /** * jQuery plugin to fade or snap to visible state. @@ -9,7 +9,7 @@ * @param boolean instantToggle (optional) * @return jQuery */ -$.fn.goIn = function( instantToggle ) { +$.fn.goIn = function ( instantToggle ) { if ( instantToggle === true ) { return $(this).show(); } @@ -22,7 +22,7 @@ $.fn.goIn = function( instantToggle ) { * @param boolean instantToggle (optional) * @return jQuery */ -$.fn.goOut = function( instantToggle ) { +$.fn.goOut = function ( instantToggle ) { if ( instantToggle === true ) { return $(this).hide(); } @@ -31,24 +31,24 @@ $.fn.goOut = function( instantToggle ) { /** * Bind a function to the jQuery object via live(), and also immediately trigger - * the function on the objects with an 'instant' paramter set to true - * @param callback function taking one paramter, which is Bool true when the event + * 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 ){ +$.fn.liveAndTestAtStart = function ( callback ){ $(this) .live( 'change', callback ) - .each( function( index, element ){ + .each( function ( index, element ){ callback.call( this, true ); } ); }; // Document ready: -$( function() { +$( function () { // Animate the SelectOrOther fields, to only show the text field when // 'other' is selected. - $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function( instant ) { + $( '.mw-htmlform-select-or-other' ).liveAndTestAtStart( function ( instant ) { var $other = $( '#' + $(this).attr( 'id' ) + '-other' ); $other = $other.add( $other.siblings( 'br' ) ); if ( $(this).val() === 'other' ) { @@ -61,4 +61,4 @@ $( function() { }); -})( jQuery ); +}( jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js index 6c00bd15..86af31ff 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.js +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -1,22 +1,27 @@ /** - * Experimental advanced wikitext parser-emitter. - * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs - * - * @author neilk@wikimedia.org - */ - -( function( mw, $, undefined ) { - - mw.jqueryMsg = {}; +* Experimental advanced wikitext parser-emitter. +* See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs +* +* @author neilk@wikimedia.org +*/ +( function ( mw, $ ) { + var slice = Array.prototype.slice, + parserDefaults = { + magic : { + 'SITENAME' : mw.config.get( 'wgSiteName' ) + }, + messages : mw.messages, + language : mw.language + }; /** * Given parser options, return a function that parses a key and replacements, returning jQuery object * @param {Object} parser options * @return {Function} accepting ( String message key, String replacement1, String replacement2 ... ) and returning {jQuery} */ - function getFailableParserFn( options ) { - var parser = new mw.jqueryMsg.parser( options ); - /** + function getFailableParserFn( options ) { + var parser = new mw.jqueryMsg.parser( options ); + /** * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes. * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it. @@ -24,24 +29,23 @@ * @param {Array} first element is the key, replacements may be in array in 2nd element, or remaining elements. * @return {jQuery} */ - return function( args ) { + return function ( args ) { var key = args[0]; - var argsArray = $.isArray( args[1] ) ? args[1] : $.makeArray( args ).slice( 1 ); - var escapedArgsArray = $.map( argsArray, function( arg ) { - return typeof arg === 'string' ? mw.html.escape( arg ) : arg; - } ); + var argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 ); try { - return parser.parse( key, escapedArgsArray ); + return parser.parse( key, argsArray ); } catch ( e ) { - return $( '' ).append( key + ': ' + e.message ); + return $( '' ).append( key + ': ' + e.message ); } }; } + mw.jqueryMsg = {}; + /** - * Class method. + * Class method. * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements). - * e.g. + * e.g. * window.gM = mediaWiki.parser.getMessageFunction( options ); * $( 'p#headline' ).html( gM( 'hello-user', username ) ); * @@ -51,75 +55,68 @@ * @param {Array} parser options * @return {Function} function suitable for assigning to window.gM */ - mw.jqueryMsg.getMessageFunction = function( options ) { + mw.jqueryMsg.getMessageFunction = function ( options ) { var failableParserFn = getFailableParserFn( options ); - /** + /** * 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) + * 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 */ - return function( /* key, replacements */ ) { + return function ( /* key, replacements */ ) { return failableParserFn( arguments ).html(); }; }; /** - * Class method. - * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to - * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links. - * e.g. + * Class method. + * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to + * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links. + * e.g. * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options ); - * var userlink = $( '
    ' ).click( function() { alert( "hello!!") } ); + * var userlink = $( '' ).click( function () { alert( "hello!!") } ); * $( 'p#headline' ).msg( 'hello-user', userlink ); * * @param {Array} parser options * @return {Function} function suitable for assigning to jQuery plugin, such as $.fn.msg */ - mw.jqueryMsg.getPlugin = function( options ) { + mw.jqueryMsg.getPlugin = function ( options ) { var failableParserFn = getFailableParserFn( options ); - /** + /** * 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) + * is equivalent to * 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) * @return {jQuery} this */ - return function( /* key, replacements */ ) { + return function ( /* key, replacements */ ) { var $target = this.empty(); - $.each( failableParserFn( arguments ).contents(), function( i, node ) { + $.each( failableParserFn( arguments ).contents(), function ( i, node ) { $target.append( node ); } ); return $target; }; }; - var parserDefaults = { - 'magic' : {}, - 'messages' : mw.messages, - 'language' : mw.language - }; - /** * The parser itself. * Describes an object, whose primary duty is to .parse() message keys. * @param {Array} options */ - mw.jqueryMsg.parser = function( options ) { + mw.jqueryMsg.parser = function ( options ) { this.settings = $.extend( {}, parserDefaults, options ); 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). astCache: {}, @@ -132,51 +129,46 @@ * @param {Array} replacements for $1, $2... $n * @return {jQuery} */ - parse: function( key, replacements ) { + parse: function ( key, replacements ) { return this.emitter.emit( this.getAst( key ), replacements ); }, - /** - * Fetch the message string associated with a key, return parsed structure. Memoized. - * Note that we pass '[' + key + ']' back for a missing message here. - * @param {String} key + * Fetch the message string associated with a key, return parsed structure. Memoized. + * Note that we pass '[' + key + ']' back for a missing message here. + * @param {String} key * @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 ) { + getAst: function ( key ) { + if ( this.astCache[ key ] === undefined ) { var wikiText = this.settings.messages.get( key ); if ( typeof wikiText !== 'string' ) { wikiText = "\\[" + key + "\\]"; } this.astCache[ key ] = this.wikiTextToAst( wikiText ); } - return this.astCache[ key ]; + return this.astCache[ key ]; }, - /* * 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. * n.b. We want to move this functionality to the server. Nothing here is required to be on the client. - * + * * @param {String} message string wikitext * @throws Error * @return {Mixed} abstract syntax tree */ - wikiTextToAst: function( input ) { - - // Indicates current position in input as we parse through it. - // Shared among all parsing functions below. - var pos = 0; + wikiTextToAst: function ( input ) { + // Indicates current position in input as we parse through it. + // Shared among all parsing functions below. + var pos = 0; // ========================================================= // parsing combinators - could be a library on its own // ========================================================= - - - // Try parsers until one works, if none work return null + // Try parsers until one works, if none work return null function choice( ps ) { - return function() { + return function () { for ( var i = 0; i < ps.length; i++ ) { var result = ps[i](); if ( result !== null ) { @@ -186,27 +178,25 @@ return null; }; } - // 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++ ) { + for ( var i = 0; i < ps.length; i++ ) { var res = ps[i](); if ( res === null ) { pos = originalPos; return null; - } + } result.push( res ); } return result; } - // run the same parser over and over until it fails. // must succeed a minimum of n times or return null function nOrMore( n, p ) { - return function() { + return function () { var originalPos = pos; var result = []; var parsed = p(); @@ -217,26 +207,23 @@ if ( result.length < n ) { pos = originalPos; return null; - } + } return result; }; } - // There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null. // But using this as a combinator seems to cause problems when combined with nOrMore(). // May be some scoping issue function transform( p, fn ) { - return function() { + return function () { var result = p(); return result === null ? null : fn( result ); }; } - // Helpers -- just make ps out of simpler JS builtin types - - function makeStringParser( s ) { + function makeStringParser( s ) { var len = s.length; - return function() { + return function () { var result = null; if ( input.substr( pos, len ) === s ) { result = s; @@ -245,105 +232,87 @@ return result; }; } - function makeRegexParser( regex ) { - return function() { + return function () { var matches = input.substr( pos ).match( regex ); - if ( matches === null ) { + if ( matches === null ) { return null; - } + } pos += matches[0].length; return matches[0]; }; } - - /** - * =================================================================== + /** + * =================================================================== * General patterns above this line -- wikitext specific parsers below - * =================================================================== + * =================================================================== */ - // Parsing functions follow. All parsing functions work like this: // They don't accept any arguments. // Instead, they just operate non destructively on the string 'input' // As they can consume parts of the string, they advance the shared variable pos, // and return tokens (or whatever else they want to return). - // some things are defined as closures and other things as ordinary functions // converting everything to a closure makes it a lot harder to debug... errors pop up // 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 regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ ); + var regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/); + var regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/); var backslash = makeStringParser( "\\" ); var anyCharacter = makeRegexParser( /^./ ); - function escapedLiteral() { var result = sequence( [ - backslash, + backslash, anyCharacter ] ); return result === null ? null : result[1]; } - var escapedOrLiteralWithoutSpace = choice( [ escapedLiteral, regularLiteralWithoutSpace ] ); - var escapedOrLiteralWithoutBar = choice( [ escapedLiteral, regularLiteralWithoutBar ] ); - - var escapedOrRegularLiteral = choice( [ + var 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(''); } - - // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default + // 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(''); } - function literal() { var result = nOrMore( 1, escapedOrRegularLiteral )(); return result === null ? null : result.join(''); } - - var whitespace = makeRegexParser( /^\s+/ ); + var whitespace = makeRegexParser( /^\s+/ ); var dollar = makeStringParser( '$' ); - var digits = makeRegexParser( /^\d+/ ); + var digits = makeRegexParser( /^\d+/ ); function replacement() { var result = sequence( [ dollar, digits ] ); - if ( result === null ) { + if ( result === null ) { return null; } return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ]; } - - var openExtlink = makeStringParser( '[' ); var closeExtlink = makeStringParser( ']' ); - // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed function extlink() { var result = null; @@ -359,10 +328,23 @@ } return result; } - + // this is the same as the above extlink, except that the url is being passed on as a parameter + function extLinkParam() { + var result = sequence( [ + openExtlink, + dollar, + digits, + whitespace, + expression, + closeExtlink + ] ); + if ( result === null ) { + return null; + } + return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ]; + } var openLink = makeStringParser( '[[' ); var closeLink = makeStringParser( ']]' ); - function link() { var result = null; var parsedResult = sequence( [ @@ -375,16 +357,14 @@ } return result; } - - var templateName = transform( + var 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(); } + makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ), + function ( result ) { return result.toString(); } ); - function templateParam() { - var result = sequence( [ + var result = sequence( [ pipe, nOrMore( 0, paramExpression ) ] ); @@ -395,9 +375,7 @@ // 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, @@ -406,21 +384,29 @@ ] ); return result === null ? null : [ result[0], result[2] ]; } - + function templateWithOutReplacement() { + var result = sequence( [ + templateName, + colon, + paramExpression + ] ); + return result === null ? null : [ result[0], result[2] ]; + } var colon = makeStringParser(':'); - var templateContents = choice( [ - function() { + function () { var res = sequence( [ - templateWithReplacement, + // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}} + // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}} + choice( [ templateWithReplacement, templateWithOutReplacement ] ), nOrMore( 0, templateParam ) ] ); return res === null ? null : res[0].concat( res[1] ); }, - function() { + function () { var res = sequence( [ templateName, - nOrMore( 0, templateParam ) + nOrMore( 0, templateParam ) ] ); if ( res === null ) { return null; @@ -428,10 +414,8 @@ return [ res[0] ].concat( res[1] ); } ] ); - var openTemplate = makeStringParser('{{'); var closeTemplate = makeStringParser('}}'); - function template() { var result = sequence( [ openTemplate, @@ -440,31 +424,30 @@ ] ); return result === null ? null : result[1]; } - var nonWhitespaceExpression = choice( [ - template, + template, link, + extLinkParam, extlink, replacement, literalWithoutSpace ] ); - var paramExpression = choice( [ - template, + template, link, + extLinkParam, extlink, replacement, literalWithoutBar ] ); - - var expression = choice( [ + var expression = choice( [ template, link, + extLinkParam, extlink, replacement, - literal + literal ] ); - function start() { var result = nOrMore( 0, expression )(); if ( result === null ) { @@ -472,16 +455,13 @@ } 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(); - + /* - * For success, the p must have gotten to the end of the input + * 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. */ @@ -490,20 +470,19 @@ } return result; } - - }; + }; /** * htmlEmitter - object which primarily exists to emit HTML from parser ASTs */ - mw.jqueryMsg.htmlEmitter = function( language, magic ) { + mw.jqueryMsg.htmlEmitter = function ( language, magic ) { this.language = language; - var _this = this; - - $.each( magic, function( key, val ) { - _this[ key.toLowerCase() ] = function() { return val; }; + var jmsg = this; + $.each( magic, function ( key, val ) { + jmsg[ key.toLowerCase() ] = function () { + return val; + }; } ); - /** * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.) * Walk entire node structure, applying replacements and template functions when appropriate @@ -511,23 +490,23 @@ * @param {Array} replacements for $1, $2, ... $n * @return {Mixed} single-string node or array of nodes suitable for jQuery appending */ - this.emit = function( node, replacements ) { + this.emit = function ( node, replacements ) { var ret = null; - var _this = this; - switch( typeof node ) { + var 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 ) { - return _this.emit( n, replacements ); + var subnodes = $.map( node.slice( 1 ), function ( n ) { + return jmsg.emit( n, replacements ); } ); var operation = node[0].toLowerCase(); - if ( typeof _this[operation] === 'function' ) { - ret = _this[ operation ]( subnodes, replacements ); + if ( typeof jmsg[operation] === 'function' ) { + ret = jmsg[ operation ]( subnodes, replacements ); } else { - throw new Error( 'unknown operation "' + operation + '"' ); + throw new Error( 'Unknown operation "' + operation + '"' ); } break; case 'undefined': @@ -537,21 +516,18 @@ ret = ''; break; default: - throw new Error( 'unexpected type in AST: ' + typeof node ); + throw new Error( 'Unexpected type in AST: ' + typeof node ); } return ret; }; - }; - // For everything in input that follows double-open-curly braces, there should be an equivalent parser - // function. For instance {{PLURAL ... }} will be processed by 'plural'. + // function. For instance {{PLURAL ... }} will be processed by 'plural'. // If you have 'magic words' then configure the parser to have them upon creation. // // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to). // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on) mw.jqueryMsg.htmlEmitter.prototype = { - /** * Parsing has been applied depth-first we can assume that all nodes here are single nodes * Must return a single node to parents -- a jQuery with synthetic span @@ -559,23 +535,23 @@ * @param {Array} nodes - mixed, some single nodes, some arrays of nodes * @return {jQuery} */ - concat: function( nodes ) { - var span = $( '' ).addClass( 'mediaWiki_htmlEmitter' ); - $.each( nodes, function( i, node ) { + concat: function ( nodes ) { + var $span = $( '' ).addClass( 'mediaWiki_htmlEmitter' ); + $.each( nodes, function ( i, node ) { if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) { - $.each( node.contents(), function( j, childNode ) { - span.append( childNode ); + $.each( node.contents(), function ( j, childNode ) { + $span.append( childNode ); } ); } else { // strings, integers, anything else - span.append( node ); + $span.append( node ); } } ); - return span; + return $span; }, /** - * Return replacement of correct index, or string if unavailable. + * Return escaped replacement of correct index, or string if unavailable. * 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" ) @@ -583,17 +559,29 @@ * @param {Array} of one element, integer, n >= 0 * @return {String} replacement */ - replace: function( nodes, replacements ) { + replace: function ( nodes, replacements ) { var index = parseInt( nodes[0], 10 ); - return index < replacements.length ? replacements[index] : '$' + ( index + 1 ); + + 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]; + } + } else { + // index not found, fallback to displaying variable + return '$' + ( index + 1 ); + } }, - /** + /** * Transform wiki-link - * TODO unimplemented + * TODO unimplemented */ - wlink: function( nodes ) { - return "unimplemented"; + wlink: function ( nodes ) { + return 'unimplemented'; }, /** @@ -601,14 +589,14 @@ * If the href is a jQuery object, treat it as "enclosing" the link text. * ... function, treat it as the click handler * ... string, treat it as a URI - * TODO: throw an error if nodes.length > 2 ? + * TODO: throw an error if nodes.length > 2 ? * @param {Array} of two elements, {jQuery|Function|String} and {String} * @return {jQuery} */ - link: function( nodes ) { + link: function ( nodes ) { var arg = nodes[0]; var contents = nodes[1]; - var $el; + var $el; if ( arg instanceof jQuery ) { $el = arg; } else { @@ -619,19 +607,39 @@ $el.attr( 'href', arg.toString() ); } } - $el.append( contents ); + $el.append( contents ); return $el; }, + /** + * This is basically use a combination of replace + link (link with parameter + * as url), but we don't want to run the regular replace here-on: inserting a + * url as href-attribute of a link will automatically escape it already, so + * we don't want replace to (manually) escape it as well. + * TODO throw error if nodes.length > 1 ? + * @param {Array} of one element, integer, n >= 0 + * @return {String} replacement + */ + linkparam: function ( nodes, replacements ) { + var replacement, + index = parseInt( nodes[0], 10 ); + if ( index < replacements.length) { + replacement = replacements[index]; + } else { + replacement = '$' + ( index + 1 ); + } + return this.link( [ replacement, nodes[1] ] ); + }, + /** * Transform parsed structure into pluralization * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number). * So convert it back with the current language's convertNumber. - * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ] + * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ] * @return {String} selected pluralized form according to current language */ - plural: function( nodes ) { - var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 ); + plural: function ( nodes ) { + var count = parseFloat( this.language.convertNumber( nodes[0], true ) ); var forms = nodes.slice(1); return forms.length ? this.language.convertPlural( count, forms ) : ''; }, @@ -639,10 +647,10 @@ /** * Transform parsed structure into gender * Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}. - * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] + * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] * @return {String} selected gender form according to current language */ - gender: function( nodes ) { + gender: function ( nodes ) { var gender; if ( nodes[0] && nodes[0].options instanceof mw.Map ){ gender = nodes[0].options.get( 'gender' ); @@ -651,35 +659,40 @@ } var forms = nodes.slice(1); return this.language.gender( gender, forms ); - } + }, + /** + * Transform parsed structure into grammar conversion. + * Invoked by putting {{grammar:form|word}} in a message + * @param {Array} of nodes [{Grammar case eg: genitive}, {String word}] + * @return {String} selected grammatical form according to current language + */ + grammar: function ( nodes ) { + var form = nodes[0]; + var word = nodes[1]; + return word && form && this.language.convertGrammar( word, form ); + } }; - - // TODO figure out a way to make magic work with common globals like wgSiteName, without requiring init from library users... - // var options = { magic: { 'SITENAME' : mw.config.get( 'wgSiteName' ) } }; - - // deprecated! don't rely on gM existing. - // the window.gM ought not to be required - or if required, not required here. But moving it to extensions breaks it (?!) + // Deprecated! don't rely on gM existing. + // The window.gM ought not to be required - or if required, not required here. + // But moving it to extensions breaks it (?!) // Need to fix plugin so it could do attributes as well, then will be okay to remove this. - window.gM = mw.jqueryMsg.getMessageFunction(); - + window.gM = mw.jqueryMsg.getMessageFunction(); $.fn.msg = mw.jqueryMsg.getPlugin(); - + // Replace the default message parser with jqueryMsg var oldParser = mw.Message.prototype.parser; - mw.Message.prototype.parser = function() { + mw.Message.prototype.parser = function () { // 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 ) { // Fall back to mw.msg's simple parser return oldParser.apply( this ); } - var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } ); return messageFunction( this.key, this.parameters ); }; -} )( mediaWiki, jQuery ); +}( mediaWiki, jQuery ) ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.peg b/resources/mediawiki/mediawiki.jqueryMsg.peg index 74c57e4b..e059ed1d 100644 --- a/resources/mediawiki/mediawiki.jqueryMsg.peg +++ b/resources/mediawiki/mediawiki.jqueryMsg.peg @@ -22,11 +22,15 @@ template templateContents = twr:templateWithReplacement p:templateParam* { return twr.concat(p) } + / twr:templateWithOutReplacement p:templateParam* { return twr.concat(p) } / t:templateName p:templateParam* { return p.length ? [ t, p ] : [ t ] } templateWithReplacement = t:templateName ":" r:replacement { return [ t, r ] } +templateWithOutReplacement + = t:templateName ":" p:paramExpression { return [ t, p ] } + templateParam = "|" e:paramExpression* { return e.length > 1 ? [ "CONCAT" ].concat(e) : e[0]; } diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index 121d5399..1a72ed13 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -3,11 +3,13 @@ */ var mw = ( function ( $, undefined ) { -"use strict"; + "use strict"; /* Private Members */ - var hasOwn = Object.prototype.hasOwnProperty; + var hasOwn = Object.prototype.hasOwnProperty, + slice = Array.prototype.slice; + /* Object constructors */ /** @@ -43,13 +45,15 @@ var mw = ( function ( $, undefined ) { var results, i; if ( $.isArray( selection ) ) { - selection = $.makeArray( selection ); + selection = slice.call( selection ); results = {}; for ( i = 0; i < selection.length; i += 1 ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; - } else if ( typeof selection === 'string' ) { + } + + if ( typeof selection === 'string' ) { if ( this.values[selection] === undefined ) { if ( fallback !== undefined ) { return fallback; @@ -58,11 +62,13 @@ var mw = ( function ( $, undefined ) { } return this.values[selection]; } + if ( selection === undefined ) { return this.values; - } else { - return null; // invalid selection key } + + // invalid selection key + return null; }, /** @@ -80,7 +86,8 @@ var mw = ( function ( $, undefined ) { this.values[s] = selection[s]; } return true; - } else if ( typeof selection === 'string' && value !== undefined ) { + } + if ( typeof selection === 'string' && value !== undefined ) { this.values[selection] = value; return true; } @@ -103,9 +110,8 @@ var mw = ( function ( $, undefined ) { } } return true; - } else { - return this.values[selection] !== undefined; } + return this.values[selection] !== undefined; } }; @@ -124,7 +130,7 @@ var mw = ( function ( $, undefined ) { this.format = 'plain'; this.map = map; this.key = key; - this.parameters = parameters === undefined ? [] : $.makeArray( parameters ); + this.parameters = parameters === undefined ? [] : slice.call( parameters ); return this; } @@ -132,17 +138,17 @@ var mw = ( function ( $, undefined ) { /** * Simple message parser, does $N replacement and nothing else. * This may be overridden to provide a more complex message parser. - * + * * This function will not be called for nonexistent messages. */ - parser: function() { + parser: function () { var parameters = this.parameters; return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) { var index = parseInt( match, 10 ) - 1; return parameters[index] !== undefined ? parameters[index] : '$' + match; } ); }, - + /** * Appends (does not replace) parameters for replacement to the .parameters property. * @@ -162,7 +168,7 @@ var mw = ( function ( $, undefined ) { * * @return string Message as a string in the current form or if key does not exist. */ - toString: function() { + toString: function () { var text; if ( !this.exists() ) { @@ -186,7 +192,7 @@ var mw = ( function ( $, undefined ) { text = this.parser(); text = mw.html.escape( text ); } - + if ( this.format === 'parse' ) { text = this.parser(); } @@ -199,7 +205,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of parsed message */ - parse: function() { + parse: function () { this.format = 'parse'; return this.toString(); }, @@ -209,7 +215,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of plain message */ - plain: function() { + plain: function () { this.format = 'plain'; return this.toString(); }, @@ -219,7 +225,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of html escaped message */ - escaped: function() { + escaped: function () { this.format = 'escaped'; return this.toString(); }, @@ -229,7 +235,7 @@ var mw = ( function ( $, undefined ) { * * @return {string} String form of parsed message */ - exists: function() { + exists: function () { return this.map.exists( this.key ); } }; @@ -241,8 +247,8 @@ var mw = ( function ( $, undefined ) { * Dummy function which in debug mode can be replaced with a function that * emulates console.log in console-less environments. */ - log: function() { }, - + log: function () { }, + /** * @var constructor Make the Map constructor publicly available. */ @@ -252,7 +258,7 @@ var mw = ( function ( $, undefined ) { * @var constructor Make the Message constructor publicly available. */ Message: Message, - + /** * List of configuration values * @@ -261,25 +267,25 @@ var mw = ( function ( $, undefined ) { * in the global window object. */ config: null, - + /** * @var object * * Empty object that plugins can be installed in. */ libs: {}, - + /* Extension points */ - + legacy: {}, - + /** * Localization system */ messages: new Map(), - + /* Public Methods */ - + /** * Gets a message object, similar to wfMessage() * @@ -292,33 +298,33 @@ var mw = ( function ( $, undefined ) { var parameters; // Support variadic arguments if ( parameter_1 !== undefined ) { - parameters = $.makeArray( arguments ); + parameters = slice.call( arguments ); parameters.shift(); } else { parameters = []; } return new Message( mw.messages, key, parameters ); }, - + /** - * Gets a message string, similar to wfMsg() + * 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. */ - msg: function ( key, parameters ) { + msg: function ( /* key, parameter_1, parameter_2, .. */ ) { return mw.message.apply( mw.message, arguments ).toString(); }, - + /** * Client-side module loader which integrates with the MediaWiki ResourceLoader */ - loader: ( function() { - + loader: ( function () { + /* Private Members */ - + /** * Mapping of registered modules * @@ -335,7 +341,7 @@ var mw = ( function ( $, undefined ) { * { * 'moduleName': { * 'version': ############## (unix timestamp), - * 'dependencies': ['required.foo', 'bar.also', ...], (or) function() {} + * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} * 'group': 'somegroup', (or) null, * 'source': 'local', 'someforeignwiki', (or) null * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing' @@ -345,7 +351,7 @@ var mw = ( function ( $, undefined ) { * } * } */ - var registry = {}, + var registry = {}, /** * Mapping of sources, keyed by source-id, values are objects. * Format: @@ -362,68 +368,111 @@ var mw = ( function ( $, undefined ) { queue = [], // List of callback functions waiting for modules to be ready to be called jobs = [], - // Flag indicating that document ready has occured - ready = false, // Selector cache for the marker element. Use getMarker() to get/use the marker! $marker = null; - - /* Cache document ready status */ - - $(document).ready( function () { - ready = true; - } ); - + /* Private methods */ - + function getMarker() { // Cached ? if ( $marker ) { return $marker; - } else { - $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); - if ( $marker.length ) { - return $marker; - } - mw.log( 'getMarker> No found, inserting dynamically.' ); - $marker = $( '' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); + } + + $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); + if ( $marker.length ) { return $marker; } + mw.log( 'getMarker> No found, inserting dynamically.' ); + $marker = $( '' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); + + return $marker; + } + + /** + * 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 + */ + function addStyleTag( text, nextnode ) { + var s = document.createElement( 'style' ); + // Insert into document before setting cssText (bug 33305) + if ( nextnode ) { + // Must be inserted with native insertBefore, not $.fn.before. + // When using jQuery to insert it, like $nextnode.before( s ), + // then IE6 will throw "Access is denied" when trying to append + // to .cssText later. Some kind of weird security measure. + // http://stackoverflow.com/q/12586482/319266 + // Works: jsfiddle.net/zJzMy/1 + // Fails: jsfiddle.net/uJTQz + // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines) + if ( nextnode.jquery ) { + nextnode = nextnode.get( 0 ); + } + nextnode.parentNode.insertBefore( s, nextnode ); + } else { + document.getElementsByTagName( 'head' )[0].appendChild( s ); + } + if ( s.styleSheet ) { + // IE + s.styleSheet.cssText = text; + } else { + // Other browsers. + // (Safari sometimes borks on non-string values, + // play safe by casting to a string, just in case.) + s.appendChild( document.createTextNode( String( text ) ) ); + } + return s; + } + + /** + * Checks if certain cssText is safe to append to + * a stylesheet. + * + * Right now it only makes sure that cssText containing @import + * rules will end up in a new stylesheet (as those only work when + * placed at the start of a stylesheet; bug 35562). + * This could later be extended to take care of other bugs, such as + * the IE cssRules limit - not the same as the IE styleSheets limit). + */ + function canExpandStylesheetWith( $style, cssText ) { + return cssText.indexOf( '@import' ) === -1; } - - function addInlineCSS( css, media ) { - var $style = getMarker().prev(), - $newStyle, - attrs = { 'type': 'text/css', 'media': media }; - if ( $style.is( 'style' ) && $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) { - // There's already a dynamic
  • ' ) .text( entry.typeText ) - .attr( 'class', 'mw-debug-console-' + entry.type ) + .addClass( 'mw-debug-console-' + entry.type ) ) .append( $( '' ).html( entry.msg ) ) .append( $( '' ).text( entry.caller ) ) @@ -254,10 +270,10 @@ $table = $( '
    ' ); $( '
    #SQLTimeCall#SQLTimeCall