diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2012-05-03 13:01:35 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2012-05-03 13:01:35 +0200 |
commit | d9022f63880ce039446fba8364f68e656b7bf4cb (patch) | |
tree | 16b40fbf17bf7c9ee6f4ead25b16dd192378050a /resources/mediawiki | |
parent | 27cf83d177256813e2e802241085fce5dd0f3fb9 (diff) |
Update to MediaWiki 1.19.0
Diffstat (limited to 'resources/mediawiki')
-rw-r--r-- | resources/mediawiki/mediawiki.Uri.js | 327 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.debug.css | 185 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.debug.init.js | 3 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.debug.js | 351 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.feedback.css | 9 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.feedback.js | 242 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.feedback.spinner.gif | bin | 0 -> 1108 bytes | |||
-rw-r--r-- | resources/mediawiki/mediawiki.htmlform.js | 6 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.js | 685 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.jqueryMsg.peg | 76 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.js | 2342 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.log.js | 95 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.user.js | 12 | ||||
-rw-r--r-- | resources/mediawiki/mediawiki.util.js | 410 |
14 files changed, 3297 insertions, 1446 deletions
diff --git a/resources/mediawiki/mediawiki.Uri.js b/resources/mediawiki/mediawiki.Uri.js index 7ff8dda4..26fdfa9e 100644 --- a/resources/mediawiki/mediawiki.Uri.js +++ b/resources/mediawiki/mediawiki.Uri.js @@ -56,7 +56,7 @@ * */ -( function( $ ) { +( function( $, mw ) { /** * Function that's useful when constructing the URI string -- we frequently encounter the pattern of @@ -89,172 +89,213 @@ 'host', // www.test.com 'port', // 81 'path', // /dir/dir.2/index.htm - 'query', // q1=0&&test1&test2=value (will become { q1: 0, test1: '', test2: 'value' } ) + 'query', // q1=0&&test1&test2=value (will become { q1: '0', test1: '', test2: 'value' } ) 'fragment' // top ]; - /** - * 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. - * @param {Boolean} strict mode (when parsing a string) - */ - mw.Uri = function( uri, strictMode ) { - strictMode = !!strictMode; - if ( uri !== undefined && uri !== null || uri !== '' ) { - if ( typeof uri === 'string' ) { - this._parse( uri, strictMode ); - } else if ( typeof uri === 'object' ) { - var _this = this; - $.each( properties, function( i, property ) { - _this[property] = uri[property]; - } ); - if ( this.query === undefined ) { - this.query = {}; - } - } - } - if ( !( this.protocol && this.host && this.path ) ) { - throw new Error( 'Bad constructor arguments' ); - } - }; /** - * 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 + * 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.Uri.encode = function( s ) { - return encodeURIComponent( s ) - .replace( /!/g, '%21').replace( /'/g, '%27').replace( /\(/g, '%28') - .replace( /\)/g, '%29').replace( /\*/g, '%2A') - .replace( /%20/g, '+' ); - }; - - /** - * Standard decodeURIComponent, with '+' to space - * @param {String} string encoded for URI - * @return {String} decoded string - */ - mw.Uri.decode = function( s ) { - return decodeURIComponent( s ).replace( /\+/g, ' ' ); - }; - - mw.Uri.prototype = { + mw.UriRelative = function( documentLocation ) { /** - * Parse a string and set our properties accordingly. - * @param {String} URI - * @param {Boolean} strictness - * @return {Boolean} success + * 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. + * @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). */ - _parse: function( str, strictMode ) { - var matches = parser[ strictMode ? 'strict' : 'loose' ].exec( str ); - var uri = this; - $.each( properties, function( i, property ) { - uri[ property ] = matches[ i+1 ]; - } ); + function Uri( uri, options ) { + options = typeof options === 'object' ? options : { strictMode: !!options }; + options = $.extend( { + strictMode: false, + overrideKeys: false + }, options ); - // 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 = {}; - // using replace to iterate over a string - if ( uri.query ) { - uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { - if ( $1 ) { - var k = mw.Uri.decode( $1 ); - var v = ( $2 === '' || $2 === undefined ) ? null : mw.Uri.decode( $3 ); - if ( typeof q[ k ] === 'string' ) { - q[ k ] = [ q[ k ] ]; - } - if ( typeof q[ k ] === 'object' ) { - q[ k ].push( v ); - } else { - q[ k ] = v; - } + if ( uri !== undefined && uri !== null || uri !== '' ) { + if ( typeof uri === 'string' ) { + 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 ) { + this.query = {}; } - } ); + } } - this.query = q; - }, - /** - * Returns user and password portion of a URI. - * @return {String} - */ - getUserInfo: function() { - return cat( '', this.user, cat( ':', this.password, '' ) ); - }, + // protocol-relative URLs + if ( !this.protocol ) { + this.protocol = defaultProtocol; + } - /** - * Gets host and port portion of a URI. - * @return {String} - */ - getHostPort: function() { - return this.host + cat( ':', this.port, '' ); - }, + if ( !( this.protocol && this.host && this.path ) ) { + throw new Error( 'Bad constructor arguments' ); + } + } /** - * 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} + * 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 */ - getAuthority: function() { - return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); - }, + Uri.encode = function( s ) { + return encodeURIComponent( s ) + .replace( /!/g, '%21').replace( /'/g, '%27').replace( /\(/g, '%28') + .replace( /\)/g, '%29').replace( /\*/g, '%2A') + .replace( /%20/g, '+' ); + }; /** - * 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} + * Standard decodeURIComponent, with '+' to space + * @param {String} string encoded for URI + * @return {String} decoded string */ - getQueryString: function() { - var args = []; - $.each( this.query, function( key, val ) { - var k = mw.Uri.encode( key ); - var vals = val === null ? [ null ] : $.makeArray( val ); - $.each( vals, function( i, v ) { - args.push( k + ( v === null ? '' : '=' + mw.Uri.encode( v ) ) ); + Uri.decode = function( s ) { + return decodeURIComponent( s.replace( /\+/g, '%20' ) ); + }; + + Uri.prototype = { + + /** + * Parse a string and set our properties accordingly. + * @param {String} URI + * @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 ) { + uri[ property ] = matches[ i+1 ]; } ); - } ); - return args.join( '&' ); - }, - /** - * Returns everything after the authority section of the URI - * @return {String} - */ - getRelativePath: function() { - return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); - }, + // 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 = {}; + // using replace to iterate over a string + if ( uri.query ) { + uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { + if ( $1 ) { + var k = Uri.decode( $1 ); + var v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 ); - /** - * 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() { - return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); - }, + // If overrideKeys, always (re)set top level value. + // If not overrideKeys but this key wasn't set before, then we set it as well. + if ( options.overrideKeys || q[ k ] === undefined ) { + q[ k ] = v; - /** - * Clone this URI - * @return {Object} new URI object with same properties - */ - clone: function() { - return new mw.Uri( this ); - }, + // Use arrays if overrideKeys is false and key was already seen before + } else { + // Once before, still a string, turn into an array + if ( typeof q[ k ] === 'string' ) { + q[ k ] = [ q[ k ] ]; + } + // Add to the array + if ( $.isArray( q[ k ] ) ) { + q[ k ].push( v ); + } + } + } + } ); + } + this.query = q; + }, - /** - * Extend the query -- supply query parameters to override or add to ours - * @param {Object} query parameters in key-val form to override or add - * @return {Object} this URI object - */ - extend: function( parameters ) { - $.extend( this.query, parameters ); - return this; - } + /** + * Returns user and password portion of a URI. + * @return {String} + */ + getUserInfo: function() { + return cat( '', this.user, cat( ':', this.password, '' ) ); + }, + + /** + * Gets host and port portion of a URI. + * @return {String} + */ + getHostPort: function() { + return this.host + cat( ':', this.port, '' ); + }, + + /** + * 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} + */ + getAuthority: function() { + return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); + }, + + /** + * 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} + */ + 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 ) { + args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) ); + } ); + } ); + return args.join( '&' ); + }, + + /** + * Returns everything after the authority section of the URI + * @return {String} + */ + getRelativePath: function() { + return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); + }, + + /** + * 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() { + return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); + }, + + /** + * Clone this URI + * @return {Object} new URI object with same properties + */ + clone: function() { + return new Uri( this ); + }, + + /** + * Extend the query -- supply query parameters to override or add to ours + * @param {Object} query parameters in key-val form to override or add + * @return {Object} this URI object + */ + extend: function( parameters ) { + $.extend( this.query, parameters ); + return this; + } + }; + + var defaultProtocol = ( new Uri( documentLocation ) ).protocol; + + return Uri; }; -} )( jQuery ); + // if we are running in a browser, inject the current document location, for relative URLs + if ( document && document.location && document.location.href ) { + mw.Uri = mw.UriRelative( document.location.href ); + } + +} )( jQuery, mediaWiki ); diff --git a/resources/mediawiki/mediawiki.debug.css b/resources/mediawiki/mediawiki.debug.css new file mode 100644 index 00000000..923d4a47 --- /dev/null +++ b/resources/mediawiki/mediawiki.debug.css @@ -0,0 +1,185 @@ +.mw-debug { + width: 100%; + text-align: left; + background-color: #eee; + border-top: 1px solid #aaa; +} + +.mw-debug pre { + font-family: Monaco, "Consolas", "Lucida Console", "Courier New", monospace; + font-size: 11px; + padding: 0; + margin: 0; + background: none; + border: none; +} + +.mw-debug table { + border-spacing: 0; + width: 100%; + table-layout: fixed; +} + +.mw-debug table tr { + background-color: #fff; +} + +.mw-debug table tr:nth-child(even) { + background-color: #f9f9f9; +} + +.mw-debug table td, .mw-debug table th { + padding: 4px 10px; +} + +.mw-debug table td { + border-bottom: 1px solid #eee; + word-wrap: break-word; +} + +.mw-debug table td.nr { + text-align: right; +} + +.mw-debug table td span.stats { + color: #808080; +} + +.mw-debug ul { + margin: 0; + list-style: none; +} + +.mw-debug li { + padding: 4px 0; + width: 100%; +} + +.mw-debug-bits { + text-align: center; + border-bottom: 1px solid #aaa; +} + +.mw-debug-bit { + display: inline-block; + padding: 10px 5px; + font-size: 13px; + /* IE-hack for display: inline-block */ + zoom: 1; + *display:inline; +} + +.mw-debug-panelink { + background-color: #eee; + border-right: 1px solid #ccc; +} + +.mw-debug-panelink:first-child { + border-left: 1px solid #ccc; +} + +.mw-debug-panelink:hover { + background-color: #fefefe; + cursor: pointer; +} +.mw-debug-panelink.current { + background-color: #dedede; + +} +a.mw-debug-panelabel, +a.mw-debug-panelabel:visited { + color: #000; +} + +.mw-debug-pane { + height: 300px; + overflow: scroll; + display: none; + font-size: 11px; + background-color: #e1eff2; + box-sizing: border-box; +} + +#mw-debug-pane-debuglog, +#mw-debug-pane-request { + padding: 20px; +} + +#mw-debug-pane-request table { + width: 100%; + margin: 10px 0 30px; +} + +#mw-debug-pane-request tr, +#mw-debug-pane-request th, +#mw-debug-pane-request td, +#mw-debug-pane-request table { + border: 1px solid #D0DBB3; + border-collapse: collapse; + margin: 0; +} + +#mw-debug-pane-request th, +#mw-debug-pane-request td { + font-size: 12px; + padding: 8px 10px; +} + +#mw-debug-pane-request th { + background-color: #F1F7E2; + font-weight: bold; +} + +#mw-debug-pane-request td { + background-color: white; +} + +#mw-debug-console tr td:first-child { + font-weight: bold; + vertical-align: top; +} + +#mw-debug-console tr td:last-child { + vertical-align: top; +} + +.mw-debug-console-log { + background-color: #add8e6; +} + +.mw-debug-console-warn { + background-color: #ffa07a; +} + +.mw-debug-console-deprecated { + background-color: #ffb6c1; +} + +.mw-debug-backtrace { + padding: 5px 10px; + margin: 5px; + background-color: #dedede; +} + +.mw-debug-backtrace span { + font-weight: bold; + color: #111; +} + +.mw-debug-backtrace ul { + padding-left: 10px; +} + +.mw-debug-backtrace li { + width: auto; + padding: 0; + color: #333; + font-size: 10px; + margin-bottom: 0; + line-height: 1em; +} + +/* Cheapo hack to hide the first 3 lines of the backtrace */ +.mw-debug-backtrace li:nth-child(-n+3) { + display: none; +} diff --git a/resources/mediawiki/mediawiki.debug.init.js b/resources/mediawiki/mediawiki.debug.init.js new file mode 100644 index 00000000..0f85e80d --- /dev/null +++ b/resources/mediawiki/mediawiki.debug.init.js @@ -0,0 +1,3 @@ +jQuery( function () { + mediaWiki.Debug.init(); +} ); diff --git a/resources/mediawiki/mediawiki.debug.js b/resources/mediawiki/mediawiki.debug.js new file mode 100644 index 00000000..a2bfbcbe --- /dev/null +++ b/resources/mediawiki/mediawiki.debug.js @@ -0,0 +1,351 @@ +/** + * JavaScript for the new debug toolbar, enabled through $wgDebugToolbar. + * + * @author John Du Hart + * @since 1.19 + */ + +( function ( $, mw, undefined ) { +"use strict"; + + var hovzer = $.getFootHovzer(); + + var debug = mw.Debug = { + /** + * Toolbar container element + * + * @var {jQuery} + */ + $container: null, + + /** + * Object containing data for the debug toolbar + * + * @var {Object} + */ + data: {}, + + /** + * Initializes the debugging pane. + * Shouldn't be called before the document is ready + * (since it binds to elements on the page). + * + * @param {Object} data, defaults to 'debugInfo' from mw.config + */ + init: function ( data ) { + + this.data = data || mw.config.get( 'debugInfo' ); + this.buildHtml(); + + // Insert the container into the DOM + hovzer.$.append( this.$container ); + hovzer.update(); + + $( '.mw-debug-panelink' ).click( this.switchPane ); + }, + + /** + * Switches between panes + * + * @todo Store cookie for last pane open + * @context {Element} + * @param {jQuery.Event} e + */ + switchPane: function ( e ) { + var currentPaneId = debug.$container.data( 'currentPane' ), + requestedPaneId = $(this).prop( 'id' ).substr( 9 ), + $currentPane = $( '#mw-debug-pane-' + currentPaneId ), + $requestedPane = $( '#mw-debug-pane-' + requestedPaneId ), + hovDone = false; + + function updateHov() { + if ( !hovDone ) { + hovzer.update(); + hovDone = true; + } + } + + // Skip hash fragment handling. Prevents screen from jumping. + e.preventDefault(); + + $( this ).addClass( 'current '); + $( '.mw-debug-panelink' ).not( this ).removeClass( 'current '); + + // Hide the current pane + if ( requestedPaneId === currentPaneId ) { + $currentPane.slideUp( updateHov ); + debug.$container.data( 'currentPane', null ); + return; + } + + debug.$container.data( 'currentPane', requestedPaneId ); + + if ( currentPaneId === undefined || currentPaneId === null ) { + $requestedPane.slideDown( updateHov ); + } else { + $currentPane.hide(); + $requestedPane.show(); + updateHov(); + } + }, + + /** + * Constructs the HTML for the debugging toolbar + */ + buildHtml: function () { + var $container, $bits, panes, id; + + $container = $( '<div id="mw-debug-toolbar" class="mw-debug"></div>' ); + + $bits = $( '<div class="mw-debug-bits"></div>' ); + + /** + * Returns a jQuery element for a debug-bit div + * + * @param id + * @return {jQuery} + */ + function bitDiv( id ) { + return $( '<div>' ).attr({ + id: 'mw-debug-' + id, + 'class': 'mw-debug-bit' + }) + .appendTo( $bits ); + } + + /** + * Returns a jQuery element for a pane link + * + * @param id + * @param text + * @return {jQuery} + */ + function paneLabel( id, text ) { + return $( '<a>' ) + .attr({ + 'class': 'mw-debug-panelabel', + href: '#mw-debug-pane-' + id + }) + .text( text ); + } + + /** + * Returns a jQuery element for a debug-bit div with a for a pane link + * + * @param id CSS id snippet. Will be prefixed with 'mw-debug-' + * @param text Text to show + * @param count Optional count to show + * @return {jQuery} + */ + function paneTriggerBitDiv( id, text, count ) { + if( count ) { + text = text + ' (' + count + ')'; + } + return $( '<div>' ).attr({ + id: 'mw-debug-' + id, + 'class': 'mw-debug-bit mw-debug-panelink' + }) + .append( paneLabel( id, text ) ) + .appendTo( $bits ); + } + + paneTriggerBitDiv( 'console', 'Console', this.data.log.length ); + + paneTriggerBitDiv( 'querylist', 'Queries', this.data.queries.length ); + + paneTriggerBitDiv( 'debuglog', 'Debug log', this.data.debugLog.length ); + + paneTriggerBitDiv( 'request', 'Request' ); + + paneTriggerBitDiv( 'includes', 'PHP includes', this.data.includes.length ); + + bitDiv( 'mwversion' ) + .append( $( '<a href="//www.mediawiki.org/"></a>' ).text( 'MediaWiki' ) ) + .append( ': ' + this.data.mwVersion ); + + bitDiv( 'phpversion' ) + .append( $( '<a href="//www.php.net/"></a>' ).text( 'PHP' ) ) + .append( ': ' + this.data.phpVersion ); + + bitDiv( 'time' ) + .text( 'Time: ' + this.data.time.toFixed( 5 ) ); + + bitDiv( 'memory' ) + .text( 'Memory: ' + this.data.memory ) + .append( $( '<span title="Peak usage"></span>' ).text( ' (' + this.data.memoryPeak + ')' ) ); + + + $bits.appendTo( $container ); + + panes = { + console: this.buildConsoleTable(), + querylist: this.buildQueryTable(), + debuglog: this.buildDebugLogTable(), + request: this.buildRequestPane(), + includes: this.buildIncludesPane() + }; + + for ( id in panes ) { + if ( !panes.hasOwnProperty( id ) ) { + continue; + } + + $( '<div>' ) + .attr({ + 'class': 'mw-debug-pane', + id: 'mw-debug-pane-' + id + }) + .append( panes[id] ) + .appendTo( $container ); + } + + this.$container = $container; + }, + + /** + * Builds the console panel + */ + buildConsoleTable: function () { + var $table, entryTypeText, i, length, entry; + + $table = $( '<table id="mw-debug-console">' ); + + $('<colgroup>').css( 'width', /*padding=*/20 + ( 10*/*fontSize*/11 ) ).appendTo( $table ); + $('<colgroup>').appendTo( $table ); + $('<colgroup>').css( 'width', 350 ).appendTo( $table ); + + + entryTypeText = function( entryType ) { + switch ( entryType ) { + case 'log': + return 'Log'; + case 'warn': + return 'Warning'; + case 'deprecated': + return 'Deprecated'; + default: + return 'Unknown'; + } + }; + + for ( i = 0, length = this.data.log.length; i < length; i += 1 ) { + entry = this.data.log[i]; + entry.typeText = entryTypeText( entry.type ); + + $( '<tr>' ) + .append( $( '<td>' ) + .text( entry.typeText ) + .attr( 'class', 'mw-debug-console-' + entry.type ) + ) + .append( $( '<td>' ).html( entry.msg ) ) + .append( $( '<td>' ).text( entry.caller ) ) + .appendTo( $table ); + } + + return $table; + }, + + /** + * Query list pane + */ + buildQueryTable: function () { + var $table, i, length, query; + + $table = $( '<table id="mw-debug-querylist"></table>' ); + + $( '<tr>' ) + .append( $('<th>#</th>').css( 'width', '4em' ) ) + .append( $('<th>SQL</th>') ) + .append( $('<th>Time</th>').css( 'width', '8em' ) ) + .append( $('<th>Call</th>').css( 'width', '18em' ) ) + .appendTo( $table ); + + for ( i = 0, length = this.data.queries.length; i < length; i += 1 ) { + query = this.data.queries[i]; + + $( '<tr>' ) + .append( $( '<td>' ).text( i + 1 ) ) + .append( $( '<td>' ).text( query.sql ) ) + .append( $( '<td class="stats">' ).text( ( query.time * 1000 ).toFixed( 4 ) + 'ms' ) ) + .append( $( '<td>' ).text( query['function'] ) ) + .appendTo( $table ); + } + + + return $table; + }, + + /** + * Legacy debug log pane + */ + buildDebugLogTable: function () { + var $list, i, length, line; + $list = $( '<ul>' ); + + for ( i = 0, length = this.data.debugLog.length; i < length; i += 1 ) { + line = this.data.debugLog[i]; + $( '<li>' ) + .html( mw.html.escape( line ).replace( /\n/g, "<br />\n" ) ) + .appendTo( $list ); + } + + return $list; + }, + + /** + * Request information pane + */ + buildRequestPane: function () { + + function buildTable( title, data ) { + var $unit, $table, key; + + $unit = $( '<div>' ).append( $( '<h2>' ).text( title ) ); + + $table = $( '<table>' ).appendTo( $unit ); + + $( '<tr>' ) + .html( '<th>Key</th><th>Value</th>' ) + .appendTo( $table ); + + for ( key in data ) { + if ( !data.hasOwnProperty( key ) ) { + continue; + } + + $( '<tr>' ) + .append( $( '<th>' ).text( key ) ) + .append( $( '<td>' ).text( data[key] ) ) + .appendTo( $table ); + } + + return $unit; + } + + return $( '<div>' ) + .text( this.data.request.method + ' ' + this.data.request.url ) + .append( buildTable( 'Headers', this.data.request.headers ) ) + .append( buildTable( 'Parameters', this.data.request.params ) ); + }, + + /** + * Included files pane + */ + buildIncludesPane: function () { + var $table, i, length, file; + + $table = $( '<table>' ); + + for ( i = 0, length = this.data.includes.length; i < length; i += 1 ) { + file = this.data.includes[i]; + $( '<tr>' ) + .append( $( '<td>' ).text( file.name ) ) + .append( $( '<td class="nr">' ).text( file.size ) ) + .appendTo( $table ); + } + + return $table; + } + }; + +} )( jQuery, mediaWiki ); diff --git a/resources/mediawiki/mediawiki.feedback.css b/resources/mediawiki/mediawiki.feedback.css new file mode 100644 index 00000000..6bd47bb2 --- /dev/null +++ b/resources/mediawiki/mediawiki.feedback.css @@ -0,0 +1,9 @@ +.feedback-spinner { + display: inline-block; + zoom: 1; + *display: inline; /* IE7 and below */ + /* @embed */ + background: url(mediawiki.feedback.spinner.gif); + width: 18px; + height: 18px; +} diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js new file mode 100644 index 00000000..9a4a7298 --- /dev/null +++ b/resources/mediawiki/mediawiki.feedback.js @@ -0,0 +1,242 @@ +/** + * mediawiki.Feedback + * + * @author Ryan Kaldari, 2010 + * @author Neil Kandalgaonkar, 2010-11 + * @since 1.19 + * + * This is a way of getting simple feedback from users. It's useful + * for testing new features -- users can give you feedback without + * the difficulty of opening a whole new talk page. For this reason, + * it also tends to collect a wider range of both positive and negative + * comments. However you do need to tend to the feedback page. It will + * get long relatively quickly, and you often get multiple messages + * reporting the same issue. + * + * It takes the form of thing on your page which, when clicked, opens a small + * dialog box. Submitting that dialog box appends its contents to a + * wiki page that you specify, as a new section. + * + * Not compatible with LiquidThreads. + * + * Minimal example in how to use it: + * + * var feedback = new mw.Feedback(); + * $( '#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 ) { + /** + * 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 + */ + mw.Feedback = function( options ) { + if ( options === undefined ) { + options = {}; + } + + if ( options.api === undefined ) { + options.api = new mw.Api(); + } + + if ( options.title === undefined ) { + options.title = new mw.Title( 'Feedback' ); + } + + if ( options.dialogTitleMessageKey === undefined ) { + options.dialogTitleMessageKey = 'feedback-submit'; + } + + if ( options.bugsLink === undefined ) { + options.bugsLink = '//bugzilla.wikimedia.org/enter_bug.cgi'; + } + + if ( options.bugsListLink === undefined ) { + options.bugsListLink = '//bugzilla.wikimedia.org/query.cgi'; + } + + $.extend( this, options ); + this.setup(); + }; + + mw.Feedback.prototype = { + setup: function() { + var _this = this; + + var $feedbackPageLink = $( '<a></a>' ) + .attr( { 'href': _this.title.getUrl(), 'target': '_blank' } ) + .css( { 'white-space': 'nowrap' } ); + + var $bugNoteLink = $( '<a></a>' ).attr( { 'href': '#' } ).click( function() { _this.displayBugs(); } ); + + var $bugsListLink = $( '<a></a>' ).attr( { 'href': _this.bugsListLink, 'target': '_blank' } ); + + this.$dialog = + $( '<div style="position:relative;"></div>' ).append( + $( '<div class="feedback-mode feedback-form"></div>' ).append( + $( '<small></small>' ).append( + $( '<p></p>' ).msg( + 'feedback-bugornote', + $bugNoteLink, + _this.title.getNameText(), + $feedbackPageLink.clone() + ) + ), + $( '<div style="margin-top:1em;"></div>' ).append( + mw.msg( 'feedback-subject' ), + $( '<br/>' ), + $( '<input type="text" class="feedback-subject" name="subject" maxlength="60" style="width:99%;"/>' ) + ), + $( '<div style="margin-top:0.4em;"></div>' ).append( + mw.msg( 'feedback-message' ), + $( '<br/>' ), + $( '<textarea name="message" class="feedback-message" style="width:99%;" rows="5" cols="60"></textarea>' ) + ) + ), + $( '<div class="feedback-mode feedback-bugs"></div>' ).append( + $( '<p>' ).msg( 'feedback-bugcheck', $bugsListLink ) + ), + $( '<div class="feedback-mode feedback-submitting" style="text-align:center;margin:3em 0;"></div>' ).append( + mw.msg( 'feedback-adding' ), + $( '<br/>' ), + $( '<span class="feedback-spinner"></span>' ) + ), + $( '<div class="feedback-mode feedback-thanks" style="text-align:center;margin:1em"></div>' ).msg( + 'feedback-thanks', _this.title.getNameText(), $feedbackPageLink.clone() + ), + $( '<div class="feedback-mode feedback-error" style="position:relative;"></div>' ).append( + $( '<div class="feedback-error-msg style="color:#990000;margin-top:0.4em;"></div>' ) + ) + ); + + // undo some damage from dialog css + this.$dialog.find( 'a' ).css( { 'color': '#0645ad' } ); + + this.$dialog.dialog({ + width: 500, + autoOpen: false, + title: mw.msg( this.dialogTitleMessageKey ), + modal: true, + buttons: _this.buttons + }); + + this.subjectInput = this.$dialog.find( 'input.feedback-subject' ).get(0); + this.messageInput = this.$dialog.find( 'textarea.feedback-message' ).get(0); + + }, + + 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() { + this.display( 'submitting' ); + }, + + displayBugs: function() { + var _this = 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 } ); + }, + + displayThanks: function() { + var _this = this; + this.display( 'thanks' ); + var closeButton = {}; + closeButton[ mw.msg( 'feedback-close' ) ] = function() { _this.$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} + */ + displayForm: function( contents ) { + var _this = 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(); }; + this.$dialog.dialog( { buttons: formButtons } ); // put the buttons back + }, + + displayError: function( message ) { + var _this = this; + this.display( 'error' ); + this.$dialog.find( '.feedback-error-msg' ).msg( message ); + var closeButton = {}; + closeButton[ mw.msg( 'feedback-close' ) ] = function() { _this.$dialog.dialog( 'close' ); }; + this.$dialog.dialog( { buttons: closeButton } ); + }, + + cancel: function() { + this.$dialog.dialog( 'close' ); + }, + + submit: function() { + var _this = this; + + // get the values to submit + var subject = this.subjectInput.value; + + var message = "<small>User agent: " + navigator.userAgent + "</small>\n\n" + + this.messageInput.value; + if ( message.indexOf( '~~~' ) == -1 ) { + message += " ~~~~"; + } + + this.displaySubmitting(); + + var ok = function( result ) { + if ( result.edit !== undefined ) { + if ( result.edit.result === 'Success' ) { + _this.displayThanks(); + } else { + _this.displayError( 'feedback-error1' ); // unknown API result + } + } else { + _this.displayError( 'feedback-error2' ); // edit failed + } + }; + + var err = function( code, info ) { + _this.displayError( 'feedback-error3' ); // ajax request failed + }; + + 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. + * @param {Object} optional prefilled contents for the feedback form. Object with properties: + * subject: {String} + * message: {String} + */ + launch: function( contents ) { + this.displayForm( contents ); + this.$dialog.dialog( 'open' ); + this.subjectInput.focus(); + } + + }; + +} )( window.mediaWiki, jQuery ); diff --git a/resources/mediawiki/mediawiki.feedback.spinner.gif b/resources/mediawiki/mediawiki.feedback.spinner.gif Binary files differnew file mode 100644 index 00000000..aed0ea41 --- /dev/null +++ b/resources/mediawiki/mediawiki.feedback.spinner.gif diff --git a/resources/mediawiki/mediawiki.htmlform.js b/resources/mediawiki/mediawiki.htmlform.js index 1a6acd6f..17a02cf4 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. @@ -10,7 +10,7 @@ * @return jQuery */ $.fn.goIn = function( instantToggle ) { - if ( instantToggle !== undefined && instantToggle === true ) { + if ( instantToggle === true ) { return $(this).show(); } return $(this).stop( true, true ).fadeIn(); @@ -23,7 +23,7 @@ $.fn.goIn = function( instantToggle ) { * @return jQuery */ $.fn.goOut = function( instantToggle ) { - if ( instantToggle !== undefined && instantToggle === true ) { + if ( instantToggle === true ) { return $(this).hide(); } return $(this).stop( true, true ).fadeOut(); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.js b/resources/mediawiki/mediawiki.jqueryMsg.js new file mode 100644 index 00000000..6c00bd15 --- /dev/null +++ b/resources/mediawiki/mediawiki.jqueryMsg.js @@ -0,0 +1,685 @@ +/** + * 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 = {}; + + /** + * 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 ); + /** + * 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. + * + * @param {Array} first element is the key, replacements may be in array in 2nd element, or remaining elements. + * @return {jQuery} + */ + 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; + } ); + try { + return parser.parse( key, escapedArgsArray ); + } catch ( e ) { + return $( '<span></span>' ).append( key + ': ' + e.message ); + } + }; + } + + /** + * Class method. + * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements). + * e.g. + * window.gM = mediaWiki.parser.getMessageFunction( options ); + * $( 'p#headline' ).html( gM( 'hello-user', username ) ); + * + * Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the + * jQuery plugin version instead. This is only included for backwards compatibility with gM(). + * + * @param {Array} parser options + * @return {Function} function suitable for assigning to window.gM + */ + 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]) + * + * @param {String} message key + * @param {Array} optional replacements (can also specify variadically) + * @return {String} rendered HTML as string + */ + 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. + * $.fn.msg = mediaWiki.parser.getJqueryPlugin( options ); + * var userlink = $( '<a>' ).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 ) { + 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]) + * + * 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 */ ) { + var $target = this.empty(); + $.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 ) { + 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: {}, + + /** + * 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 + * @return {jQuery} + */ + 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 + * @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 ); + if ( typeof wikiText !== 'string' ) { + wikiText = "\\[" + key + "\\]"; + } + this.astCache[ key ] = this.wikiTextToAst( wikiText ); + } + 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; + + // ========================================================= + // 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](); + if ( result !== null ) { + return result; + } + } + 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++ ) { + 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() { + var originalPos = pos; + var result = []; + var parsed = p(); + while ( parsed !== null ) { + result.push( parsed ); + parsed = p(); + } + 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() { + var result = p(); + return result === null ? null : fn( result ); + }; + } + + // Helpers -- just make ps out of simpler JS builtin types + + function makeStringParser( s ) { + var len = s.length; + return function() { + var result = null; + if ( input.substr( pos, len ) === s ) { + result = s; + pos += len; + } + return result; + }; + } + + function makeRegexParser( regex ) { + return function() { + var matches = input.substr( pos ).match( regex ); + 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 backslash = makeStringParser( "\\" ); + var anyCharacter = makeRegexParser( /^./ ); + + function escapedLiteral() { + var result = sequence( [ + backslash, + anyCharacter + ] ); + return result === null ? null : result[1]; + } + + var escapedOrLiteralWithoutSpace = choice( [ + escapedLiteral, + regularLiteralWithoutSpace + ] ); + + var escapedOrLiteralWithoutBar = choice( [ + escapedLiteral, + regularLiteralWithoutBar + ] ); + + 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 + // 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 dollar = makeStringParser( '$' ); + var digits = makeRegexParser( /^\d+/ ); + + function replacement() { + var result = sequence( [ + dollar, + digits + ] ); + 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; + var parsedResult = sequence( [ + openExtlink, + nonWhitespaceExpression, + whitespace, + expression, + closeExtlink + ] ); + if ( parsedResult !== null ) { + result = [ 'LINK', parsedResult[1], parsedResult[3] ]; + } + return result; + } + + var openLink = makeStringParser( '[[' ); + var closeLink = makeStringParser( ']]' ); + + function link() { + var result = null; + var parsedResult = sequence( [ + openLink, + expression, + closeLink + ] ); + if ( parsedResult !== null ) { + result = [ 'WLINK', parsedResult[1] ]; + } + return result; + } + + 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(); } + ); + + function templateParam() { + var 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]; + } + + var pipe = makeStringParser( '|' ); + + function templateWithReplacement() { + var result = sequence( [ + templateName, + colon, + replacement + ] ); + return result === null ? null : [ result[0], result[2] ]; + } + + var colon = makeStringParser(':'); + + var templateContents = choice( [ + function() { + var res = sequence( [ + templateWithReplacement, + nOrMore( 0, templateParam ) + ] ); + return res === null ? null : res[0].concat( res[1] ); + }, + function() { + var res = sequence( [ + templateName, + nOrMore( 0, templateParam ) + ] ); + if ( res === null ) { + return null; + } + 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( [ + template, + link, + extlink, + replacement, + literalWithoutSpace + ] ); + + var paramExpression = choice( [ + template, + link, + extlink, + replacement, + literalWithoutBar + ] ); + + var expression = choice( [ + template, + link, + extlink, + replacement, + literal + ] ); + + function start() { + var result = nOrMore( 0, expression )(); + if ( result === null ) { + return null; + } + 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 + * 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 ); + } + return result; + } + + }; + + /** + * htmlEmitter - object which primarily exists to emit HTML from parser ASTs + */ + mw.jqueryMsg.htmlEmitter = function( language, magic ) { + this.language = language; + var _this = this; + + $.each( magic, function( key, val ) { + _this[ 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 + * @param {Mixed} abstract syntax tree (top node or subnode) + * @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 ) { + var ret = null; + var _this = 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 operation = node[0].toLowerCase(); + if ( typeof _this[operation] === 'function' ) { + ret = _this[ operation ]( subnodes, replacements ); + } else { + throw new Error( 'unknown operation "' + operation + '"' ); + } + break; + case 'undefined': + // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined + // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information? + // The logical thing is probably to return the empty string here when we encounter undefined. + ret = ''; + break; + default: + 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'. + // 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 + * However, unwrap any other synthetic spans in our children and pass them upwards + * @param {Array} nodes - mixed, some single nodes, some arrays of nodes + * @return {jQuery} + */ + concat: function( nodes ) { + var span = $( '<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 ); + } ); + } else { + // strings, integers, anything else + span.append( node ); + } + } ); + return span; + }, + + /** + * Return 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" ) + * TODO throw error if nodes.length > 1 ? + * @param {Array} of one element, integer, n >= 0 + * @return {String} replacement + */ + replace: function( nodes, replacements ) { + var index = parseInt( nodes[0], 10 ); + return index < replacements.length ? replacements[index] : '$' + ( index + 1 ); + }, + + /** + * Transform wiki-link + * TODO unimplemented + */ + wlink: function( nodes ) { + return "unimplemented"; + }, + + /** + * Transform parsed structure into external link + * 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 ? + * @param {Array} of two elements, {jQuery|Function|String} and {String} + * @return {jQuery} + */ + link: function( nodes ) { + var arg = nodes[0]; + var contents = nodes[1]; + var $el; + if ( arg instanceof jQuery ) { + $el = arg; + } else { + $el = $( '<a>' ); + if ( typeof arg === 'function' ) { + $el.click( arg ).attr( 'href', '#' ); + } else { + $el.attr( 'href', arg.toString() ); + } + } + $el.append( contents ); + return $el; + }, + + /** + * 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} ... ] + * @return {String} selected pluralized form according to current language + */ + plural: function( nodes ) { + var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 ); + var 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} ] + * @return {String} selected gender form according to current language + */ + gender: function( nodes ) { + var gender; + if ( nodes[0] && nodes[0].options instanceof mw.Map ){ + gender = nodes[0].options.get( 'gender' ); + } else { + gender = nodes[0]; + } + var forms = nodes.slice(1); + return this.language.gender( gender, forms ); + } + + }; + + // 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 (?!) + // Need to fix plugin so it could do attributes as well, then will be okay to remove this. + 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() { + // 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 ); diff --git a/resources/mediawiki/mediawiki.jqueryMsg.peg b/resources/mediawiki/mediawiki.jqueryMsg.peg new file mode 100644 index 00000000..74c57e4b --- /dev/null +++ b/resources/mediawiki/mediawiki.jqueryMsg.peg @@ -0,0 +1,76 @@ +/* PEG grammar for a subset of wikitext, useful in the MediaWiki frontend */ + +start + = e:expression* { return e.length > 1 ? [ "CONCAT" ].concat(e) : e[0]; } + +expression + = template + / link + / extlink + / replacement + / literal + +paramExpression + = template + / link + / extlink + / replacement + / literalWithoutBar + +template + = "{{" t:templateContents "}}" { return t; } + +templateContents + = twr:templateWithReplacement p:templateParam* { return twr.concat(p) } + / t:templateName p:templateParam* { return p.length ? [ t, p ] : [ t ] } + +templateWithReplacement + = t:templateName ":" r:replacement { return [ t, r ] } + +templateParam + = "|" e:paramExpression* { return e.length > 1 ? [ "CONCAT" ].concat(e) : e[0]; } + +templateName + = tn:[A-Za-z_]+ { return tn.join('').toUpperCase() } + +link + = "[[" w:expression "]]" { return [ 'WLINK', w ]; } + +extlink + = "[" url:url whitespace text:expression "]" { return [ 'LINK', url, text ] } + +url + = url:[^ ]+ { return url.join(''); } + +whitespace + = [ ]+ + +replacement + = '$' digits:digits { return [ 'REPLACE', parseInt( digits, 10 ) - 1 ] } + +digits + = [0-9]+ + +literal + = lit:escapedOrRegularLiteral+ { return lit.join(''); } + +literalWithoutBar + = lit:escapedOrLiteralWithoutBar+ { return lit.join(''); } + +escapedOrRegularLiteral + = escapedLiteral + / regularLiteral + +escapedOrLiteralWithoutBar + = escapedLiteral + / regularLiteralWithoutBar + +escapedLiteral + = "\\" escaped:. { return escaped; } + +regularLiteral + = [^{}\[\]$\\] + +regularLiteralWithoutBar + = [^{}\[\]$\\|] + diff --git a/resources/mediawiki/mediawiki.js b/resources/mediawiki/mediawiki.js index aca59e4e..121d5399 100644 --- a/resources/mediawiki/mediawiki.js +++ b/resources/mediawiki/mediawiki.js @@ -2,16 +2,12 @@ * Core MediaWiki JavaScript Library */ -// Attach to window -window.mediaWiki = new ( function( $ ) { +var mw = ( function ( $, undefined ) { +"use strict"; /* Private Members */ - /** - * @var object List of messages that have been requested to be loaded. - */ - var messageQueue = {}; - + var hasOwn = Object.prototype.hasOwnProperty; /* Object constructors */ /** @@ -21,87 +17,95 @@ window.mediaWiki = new ( function( $ ) { * that allow both single and multiple variables at once. * * @param global boolean Whether to store the values in the global window - * object or a exclusively in the object property 'values'. + * object or a exclusively in the object property 'values'. * @return Map */ function Map( global ) { - this.values = ( global === true ) ? window : {}; + this.values = global === true ? window : {}; return this; } - /** - * Get the value of one or multiple a keys. - * - * If called with no arguments, all values will be returned. - * - * @param selection mixed String key or array of keys to get values for. - * @param fallback mixed Value to use in case key(s) do not exist (optional). - * @return mixed If selection was a string returns the value or null, - * 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. - */ - Map.prototype.get = function( selection, fallback ) { - if ( $.isArray( selection ) ) { - selection = $.makeArray( selection ); - var results = {}; - for ( var i = 0; i < selection.length; i++ ) { - results[selection[i]] = this.get( selection[i], fallback ); - } - return results; - } else if ( typeof selection === 'string' ) { - if ( this.values[selection] === undefined ) { - if ( fallback !== undefined ) { - return fallback; + Map.prototype = { + /** + * Get the value of one or multiple a keys. + * + * If called with no arguments, all values will be returned. + * + * @param selection mixed String key or array of keys to get values for. + * @param fallback mixed Value to use in case key(s) do not exist (optional). + * @return mixed If selection was a string returns the value or null, + * 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. + */ + get: function ( selection, fallback ) { + var results, i; + + if ( $.isArray( selection ) ) { + selection = $.makeArray( selection ); + results = {}; + for ( i = 0; i < selection.length; i += 1 ) { + results[selection[i]] = this.get( selection[i], fallback ); } - return null; + return results; + } else if ( typeof selection === 'string' ) { + if ( this.values[selection] === undefined ) { + if ( fallback !== undefined ) { + return fallback; + } + return null; + } + return this.values[selection]; } - return this.values[selection]; - } - if ( selection === undefined ) { - return this.values; - } else { - return null; // invalid selection key - } - }; + if ( selection === undefined ) { + return this.values; + } else { + return null; // invalid selection key + } + }, - /** - * Sets one or multiple key/value pairs. - * - * @param selection mixed String key or array of keys to set values for. - * @param value mixed Value to set (optional, only in use when key is a string) - * @return bool This returns true on success, false on failure. - */ - Map.prototype.set = function( selection, value ) { - if ( $.isPlainObject( selection ) ) { - for ( var s in selection ) { - this.values[s] = selection[s]; + /** + * Sets one or multiple key/value pairs. + * + * @param selection {mixed} String key or array of keys to set values for. + * @param value {mixed} Value to set (optional, only in use when key is a string) + * @return {Boolean} This returns true on success, false on failure. + */ + set: function ( selection, value ) { + var s; + + if ( $.isPlainObject( selection ) ) { + for ( s in selection ) { + this.values[s] = selection[s]; + } + return true; + } else if ( typeof selection === 'string' && value !== undefined ) { + this.values[selection] = value; + return true; } - return true; - } else if ( typeof selection === 'string' && value !== undefined ) { - this.values[selection] = value; - return true; - } - return false; - }; + return false; + }, - /** - * Checks if one or multiple keys exist. - * - * @param selection mixed String key or array of keys to check - * @return boolean Existence of key(s) - */ - Map.prototype.exists = function( selection ) { - if ( typeof selection === 'object' ) { - for ( var s = 0; s < selection.length; s++ ) { - if ( !( selection[s] in this.values ) ) { - return false; + /** + * Checks if one or multiple keys exist. + * + * @param selection {mixed} String key or array of keys to check + * @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 ) { + return false; + } } + return true; + } else { + return this.values[selection] !== undefined; } - return true; - } else { - return selection in this.values; } }; @@ -117,418 +121,630 @@ window.mediaWiki = new ( function( $ ) { * @return Message */ function Message( map, key, parameters ) { - this.format = 'parse'; + this.format = 'plain'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : $.makeArray( parameters ); return this; } - /** - * Appends (does not replace) parameters for replacement to the .parameters property. - * - * @param parameters Array - * @return Message - */ - Message.prototype.params = function( parameters ) { - for ( var i = 0; i < parameters.length; i++ ) { - this.parameters.push( parameters[i] ); - } - return this; - }; - - /** - * 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 <key> if key does not exist. - */ - Message.prototype.toString = function() { - if ( !this.map.exists( this.key ) ) { - // Use <key> as text if key does not exist - if ( this.format !== 'plain' ) { - // format 'escape' and 'parse' need to have the brackets and key html escaped - return mw.html.escape( '<' + this.key + '>' ); + Message.prototype = { + /** + * 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() { + 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. + * + * @param parameters Array + * @return Message + */ + params: function ( parameters ) { + var i; + for ( i = 0; i < parameters.length; i += 1 ) { + this.parameters.push( parameters[i] ); } - return '<' + this.key + '>'; - } - var text = this.map.get( this.key ); - var parameters = this.parameters; - text = text.replace( /\$(\d+)/g, function( string, match ) { - var index = parseInt( match, 10 ) - 1; - return index in parameters ? parameters[index] : '$' + match; - } ); - - if ( this.format === 'plain' ) { - return text; - } - if ( this.format === 'escaped' ) { - // According to Message.php this needs {{-transformation, which is - // still todo - return mw.html.escape( text ); - } - - /* This should be fixed up when we have a parser - if ( this.format === 'parse' && 'language' in mediaWiki ) { - text = mw.language.parse( text ); - } - */ - return text; - }; - - /** - * Changes format to parse and converts message to string - * - * @return {string} String form of parsed message - */ - Message.prototype.parse = function() { - this.format = 'parse'; - return this.toString(); - }; - - /** - * Changes format to plain and converts message to string - * - * @return {string} String form of plain message - */ - Message.prototype.plain = function() { - this.format = 'plain'; - return this.toString(); - }; - - /** - * Changes the format to html escaped and converts message to string - * - * @return {string} String form of html escaped message - */ - Message.prototype.escaped = function() { - this.format = 'escaped'; - return this.toString(); - }; - - /** - * Checks if message exists - * - * @return {string} String form of parsed message - */ - Message.prototype.exists = function() { - return this.map.exists( this.key ); - }; + return this; + }, - /* Public Members */ + /** + * 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 <key> if key does not exist. + */ + toString: function() { + var text; + + if ( !this.exists() ) { + // Use <key> as text if key does not exist + if ( this.format !== 'plain' ) { + // format 'escape' and 'parse' need to have the brackets and key html escaped + return mw.html.escape( '<' + this.key + '>' ); + } + return '<' + this.key + '>'; + } - /* - * Dummy function which in debug mode can be replaced with a function that - * emulates console.log in console-less environments. - */ - this.log = function() { }; + 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. + text = this.parser(); + } - /** - * @var constructor Make the Map-class publicly available. - */ - this.Map = Map; + if ( this.format === 'escaped' ) { + text = this.parser(); + text = mw.html.escape( text ); + } + + if ( this.format === 'parse' ) { + text = this.parser(); + } - /** - * 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 - * in the global window object. - */ - this.config = null; + return text; + }, - /** - * @var object - * - * Empty object that plugins can be installed in. - */ - this.libs = {}; + /** + * Changes format to parse and converts message to string + * + * @return {string} String form of parsed message + */ + parse: function() { + this.format = 'parse'; + return this.toString(); + }, - /* - * Localization system - */ - this.messages = new this.Map(); + /** + * Changes format to plain and converts message to string + * + * @return {string} String form of plain message + */ + plain: function() { + this.format = 'plain'; + return this.toString(); + }, - /* Public Methods */ + /** + * Changes the format to html escaped and converts message to string + * + * @return {string} String form of html escaped message + */ + escaped: function() { + this.format = 'escaped'; + return this.toString(); + }, - /** - * 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 - */ - this.message = function( key, parameter_1 /* [, parameter_2] */ ) { - var parameters; - // Support variadic arguments - if ( parameter_1 !== undefined ) { - parameters = $.makeArray( arguments ); - parameters.shift(); - } else { - parameters = []; + /** + * Checks if message exists + * + * @return {string} String form of parsed message + */ + exists: function() { + return this.map.exists( this.key ); } - return new Message( mw.messages, key, parameters ); }; - /** - * Gets a message string, similar to wfMsg() - * - * @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. - */ - this.msg = function( key, parameters ) { - return mw.message.apply( mw.message, arguments ).toString(); - }; + return { + /* Public Members */ - /** - * Client-side module loader which integrates with the MediaWiki ResourceLoader - */ - this.loader = new ( function() { - - /* Private Members */ + /** + * Dummy function which in debug mode can be replaced with a function that + * emulates console.log in console-less environments. + */ + log: function() { }, + + /** + * @var constructor Make the Map constructor publicly available. + */ + Map: Map, /** - * Mapping of registered modules + * @var constructor Make the Message constructor publicly available. + */ + Message: Message, + + /** + * List of configuration values * - * The jquery module is pre-registered, because it must have already - * been provided for this object to have been built, and in debug mode - * jquery would have been provided through a unique loader request, - * making it impossible to hold back registration of jquery until after - * mediawiki. + * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). + * If $wgLegacyJavaScriptGlobals is true, this Map will have its values + * in the global window object. + */ + config: null, + + /** + * @var object * - * Format: - * { - * 'moduleName': { - * 'dependencies': ['required module', 'required module', ...], (or) function() {} - * 'state': 'registered', 'loading', 'loaded', 'ready', or 'error' - * 'script': function() {}, - * 'style': 'css code string', - * 'messages': { 'key': 'value' }, - * 'version': ############## (unix timestamp) - * } - * } + * Empty object that plugins can be installed in. */ - var registry = {}; - // List of modules which will be loaded as when ready - var batch = []; - // List of modules to be loaded - var queue = []; - // List of callback functions waiting for modules to be ready to be called - var jobs = []; - // Flag inidicating that document ready has occured - var ready = false; - // Selector cache for the marker element. Use getMarker() to get/use the marker! - var $marker = null; - - /* Private Methods */ - - function getMarker(){ - // Cached ? - if ( $marker ) { - return $marker; + libs: {}, + + /* Extension points */ + + legacy: {}, + + /** + * Localization system + */ + messages: new Map(), + + /* Public Methods */ + + /** + * 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 + */ + message: function ( key, parameter_1 /* [, parameter_2] */ ) { + var parameters; + // Support variadic arguments + if ( parameter_1 !== undefined ) { + parameters = $.makeArray( arguments ); + parameters.shift(); } else { - $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); - if ( $marker.length ) { + parameters = []; + } + return new Message( mw.messages, key, parameters ); + }, + + /** + * Gets a message string, similar to wfMsg() + * + * @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 ) { + return mw.message.apply( mw.message, arguments ).toString(); + }, + + /** + * Client-side module loader which integrates with the MediaWiki ResourceLoader + */ + loader: ( function() { + + /* Private Members */ + + /** + * Mapping of registered modules + * + * The jquery module is pre-registered, because it must have already + * been provided for this object to have been built, and in debug mode + * jquery would have been provided through a unique loader request, + * making it impossible to hold back registration of jquery until after + * mediawiki. + * + * For exact details on support for script, style and messages, look at + * 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' }, + * } + * } + */ + var registry = {}, + /** + * 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 = [], + // List of modules to be loaded + 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 <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' ); + $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); return $marker; } - mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' ); - $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); - return $marker; - } - } - - function compare( a, b ) { - if ( a.length != b.length ) { - return false; } - for ( var i = 0; i < b.length; i++ ) { - if ( $.isArray( a[i] ) ) { - if ( !compare( a[i], b[i] ) ) { - return false; + + 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 <style> tag present, append to it + // This recycling of <style> tags is for bug 31676 (can't have + // more than 32 <style> tags in IE) + + // Also, calling .append() on a <style> tag explodes with a JS error in IE, + // so if the .append() fails we fall back to building a new <style> tag and + // replacing the existing one + try { + // Do cdata sanitization on the provided CSS, and prepend a double newline + css = $( mw.html.element( 'style', {}, new mw.html.Cdata( "\n\n" + css ) ) ).html(); + $style.append( css ); + } catch ( e ) { + // Generate a new tag with the combined CSS + css = $style.html() + "\n\n" + css; + $newStyle = $( mw.html.element( 'style', attrs, new mw.html.Cdata( css ) ) ) + .data( 'ResourceLoaderDynamicStyleTag', true ); + // Prevent a flash of unstyled content by inserting the new tag + // before removing the old one + $style.after( $newStyle ); + $style.remove(); } + } else { + // Create a new <style> tag and insert it + $style = $( mw.html.element( 'style', attrs, new mw.html.Cdata( css ) ) ); + $style.data( 'ResourceLoaderDynamicStyleTag', true ); + getMarker().before( $style ); } - if ( a[i] !== b[i] ) { + } + + function compare( a, b ) { + var i; + if ( a.length !== b.length ) { return false; } + for ( i = 0; i < b.length; i += 1 ) { + if ( $.isArray( a[i] ) ) { + if ( !compare( a[i], b[i] ) ) { + return false; + } + } + if ( a[i] !== b[i] ) { + return false; + } + } + return true; } - return true; - } - - /** - * Generates an ISO8601 "basic" string from a UNIX timestamp - */ - function formatVersionNumber( timestamp ) { - function pad( a, b, c ) { - return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); - } - var d = new Date(); - d.setTime( timestamp * 1000 ); - return [ - pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T', - pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z' - ].join( '' ); - } - - /** - * Recursively resolves dependencies and detects circular references - */ - function recurse( module, resolved, unresolved ) { - if ( registry[module] === undefined ) { - throw new Error( 'Unknown dependency: ' + module ); + + /** + * Generates an ISO8601 "basic" string from a UNIX timestamp + */ + function formatVersionNumber( timestamp ) { + var pad = function ( a, b, c ) { + return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' ); + }, + d = new Date(); + d.setTime( timestamp * 1000 ); + return [ + pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T', + pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z' + ].join( '' ); } - // Resolves dynamic loader function and replaces it with its own results - if ( $.isFunction( registry[module].dependencies ) ) { - registry[module].dependencies = registry[module].dependencies(); - // Ensures the module's dependencies are always in an array - if ( typeof registry[module].dependencies !== 'object' ) { - registry[module].dependencies = [registry[module].dependencies]; + + /** + * Recursively resolves dependencies and detects circular references + */ + function recurse( module, resolved, unresolved ) { + var n, deps, len; + + if ( registry[module] === undefined ) { + throw new Error( 'Unknown dependency: ' + module ); } - } - // Tracks down dependencies - for ( var n = 0; n < registry[module].dependencies.length; n++ ) { - if ( $.inArray( registry[module].dependencies[n], resolved ) === -1 ) { - if ( $.inArray( registry[module].dependencies[n], unresolved ) !== -1 ) { - throw new Error( - 'Circular reference detected: ' + module + - ' -> ' + registry[module].dependencies[n] - ); + // Resolves dynamic loader function and replaces it with its own results + if ( $.isFunction( registry[module].dependencies ) ) { + registry[module].dependencies = registry[module].dependencies(); + // Ensures the module's dependencies are always in an array + if ( typeof registry[module].dependencies !== 'object' ) { + registry[module].dependencies = [registry[module].dependencies]; } - recurse( registry[module].dependencies[n], resolved, unresolved ); } - } - resolved[resolved.length] = module; - unresolved.splice( $.inArray( module, unresolved ), 1 ); - } + // Tracks down dependencies + deps = registry[module].dependencies; + len = deps.length; + for ( n = 0; n < len; n += 1 ) { + if ( $.inArray( deps[n], resolved ) === -1 ) { + if ( $.inArray( deps[n], unresolved ) !== -1 ) { + throw new Error( + 'Circular reference detected: ' + module + + ' -> ' + deps[n] + ); + } - /** - * Gets a list of module names that a module depends on in their proper dependency order - * - * @param module string module name or array of string module names - * @return list of dependencies - * @throws Error if circular reference is detected - */ - function resolve( module ) { - // Allow calling with an array of module names - if ( typeof module === 'object' ) { - var modules = []; - for ( var m = 0; m < module.length; m++ ) { - var dependencies = resolve( module[m] ); - for ( var n = 0; n < dependencies.length; n++ ) { - modules[modules.length] = dependencies[n]; + // Add to unresolved + unresolved[unresolved.length] = module; + recurse( deps[n], resolved, unresolved ); + // module is at the end of unresolved + unresolved.pop(); } } - return modules; - } else if ( typeof module === 'string' ) { - // Undefined modules have no dependencies - if ( !( module in registry ) ) { - return []; - } - var resolved = []; - recurse( module, resolved, [] ); - return resolved; + resolved[resolved.length] = module; } - throw new Error( 'Invalid module argument: ' + module ); - } - - /** - * Narrows a list of module names down to those matching a specific - * state. Possible states are 'undefined', 'registered', 'loading', - * 'loaded', or 'ready' - * - * @param states string or array of strings of module states to filter by - * @param modules array list of module names to filter (optional, all modules - * will be used by default) - * @return array list of filtered module names - */ - function filter( states, modules ) { - // Allow states to be given as a string - if ( typeof states === 'string' ) { - states = [states]; + + /** + * Gets a list of module names that a module depends on in their proper dependency order + * + * @param module string module name or array of string module names + * @return list of dependencies, including 'module'. + * @throws Error if circular reference is detected + */ + function resolve( module ) { + var modules, m, deps, n, resolved; + + // Allow calling with an array of module names + if ( $.isArray( module ) ) { + modules = []; + for ( m = 0; m < module.length; m += 1 ) { + deps = resolve( module[m] ); + for ( n = 0; n < deps.length; n += 1 ) { + modules[modules.length] = deps[n]; + } + } + return modules; + } else if ( typeof module === 'string' ) { + resolved = []; + recurse( module, resolved, [] ); + return resolved; + } + throw new Error( 'Invalid module argument: ' + module ); } - // If called without a list of modules, build and use a list of all modules - var list = [], module; - if ( modules === undefined ) { - modules = []; - for ( module in registry ) { - modules[modules.length] = module; + + /** + * Narrows a list of module names down to those matching a specific + * state (see comment on top of this scope for a list of valid states). + * One can also filter for 'unregistered', which will return the + * modules names that don't have a registry entry. + * + * @param states string or array of strings of module states to filter by + * @param modules array list of module names to filter (optional, by default the entire + * registry is used) + * @return array list of filtered module names + */ + function filter( states, modules ) { + var list, module, s, m; + + // Allow states to be given as a string + if ( typeof states === 'string' ) { + states = [states]; + } + // If called without a list of modules, build and use a list of all modules + list = []; + if ( modules === undefined ) { + modules = []; + for ( module in registry ) { + modules[modules.length] = module; + } } + // Build a list of modules which are in one of the specified states + for ( s = 0; s < states.length; s += 1 ) { + for ( m = 0; m < modules.length; m += 1 ) { + if ( registry[modules[m]] === undefined ) { + // Module does not exist + if ( states[s] === 'unregistered' ) { + // OK, undefined + list[list.length] = modules[m]; + } + } else { + // Module exists, check state + if ( registry[modules[m]].state === states[s] ) { + // OK, correct state + list[list.length] = modules[m]; + } + } + } + } + return list; } - // Build a list of modules which are in one of the specified states - for ( var s = 0; s < states.length; s++ ) { - for ( var m = 0; m < modules.length; m++ ) { - if ( registry[modules[m]] === undefined ) { - // Module does not exist - if ( states[s] == 'undefined' ) { - // OK, undefined - list[list.length] = modules[m]; + + /** + * Automatically executes jobs and modules which are pending with satistifed dependencies. + * + * This is used when dependencies are satisfied, such as when a module is executed. + */ + function handlePending( module ) { + var j, r; + + try { + // Run jobs whose dependencies have just been met + for ( j = 0; j < jobs.length; j += 1 ) { + if ( compare( + filter( 'ready', jobs[j].dependencies ), + jobs[j].dependencies ) ) + { + var callback = jobs[j].ready; + jobs.splice( j, 1 ); + j -= 1; + if ( $.isFunction( callback ) ) { + callback(); + } } - } else { - // Module exists, check state - if ( registry[modules[m]].state === states[s] ) { - // OK, correct state - list[list.length] = modules[m]; + } + // Execute modules whose dependencies have just been met + for ( r in registry ) { + if ( registry[r].state === 'loaded' ) { + if ( compare( + filter( ['ready'], registry[r].dependencies ), + registry[r].dependencies ) ) + { + execute( r ); + } + } + } + } catch ( e ) { + // Run error callbacks of jobs affected by this condition + for ( j = 0; j < jobs.length; j += 1 ) { + if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) { + if ( $.isFunction( jobs[j].error ) ) { + jobs[j].error( e, module ); + } + jobs.splice( j, 1 ); + j -= 1; } } + throw e; } } - return list; - } - - /** - * Executes a loaded module, making it ready to use - * - * @param module string module name to execute - */ - function execute( module, callback ) { - var _fn = 'mw.loader::execute> '; - if ( registry[module] === undefined ) { - throw new Error( 'Module has not been registered yet: ' + module ); - } else if ( registry[module].state === 'registered' ) { - throw new Error( 'Module has not been requested from the server yet: ' + module ); - } else if ( registry[module].state === 'loading' ) { - throw new Error( 'Module has not completed loading yet: ' + module ); - } else if ( registry[module].state === 'ready' ) { - throw new Error( 'Module has already been loaded: ' + module ); + + /** + * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation, + * depending on whether document-ready has occured yet and whether we are in async mode. + * + * @param src String: URL to script, will be used as the src attribute in the script tag + * @param callback Function: Optional callback which will be run when the script is done + */ + function addScript( src, callback, async ) { + var done = false, script, head; + if ( ready || async ) { + // jQuery's getScript method is NOT better than doing this the old-fashioned way + // because jQuery will eval the script's code, and errors will not have sane + // line numbers. + script = document.createElement( 'script' ); + script.setAttribute( 'src', src ); + script.setAttribute( 'type', 'text/javascript' ); + if ( $.isFunction( callback ) ) { + // Attach handlers for all browsers (based on jQuery.ajax) + script.onload = script.onreadystatechange = function() { + + if ( + !done + && ( + !script.readyState + || /loaded|complete/.test( script.readyState ) + ) + ) { + + done = true; + + callback(); + + // Handle memory leak in IE. This seems to fail in + // IE7 sometimes (Permission Denied error when + // accessing script.parentNode) so wrap it in + // a try catch. + try { + script.onload = script.onreadystatechange = null; + if ( script.parentNode ) { + script.parentNode.removeChild( script ); + } + + // Dereference the script + script = undefined; + } catch ( e ) { } + } + }; + } + + if ( window.opera ) { + // Appending to the <head> blocks rendering completely in Opera, + // so append to the <body> after document ready. This means the + // scripts only start loading after the document has been rendered, + // but so be it. Opera users don't deserve faster web pages if their + // browser makes it impossible + $( function() { document.body.appendChild( script ); } ); + } else { + // IE-safe way of getting the <head> . document.documentElement.head doesn't + // work in scripts that run in the <head> + head = document.getElementsByTagName( 'head' )[0]; + ( document.body || head ).appendChild( script ); + } + } else { + document.write( mw.html.element( + 'script', { 'type': 'text/javascript', 'src': src }, '' + ) ); + if ( $.isFunction( callback ) ) { + // Document.write is synchronous, so this is called when it's done + // FIXME: that's a lie. doc.write isn't actually synchronous + callback(); + } + } } - // Add styles - if ( $.isPlainObject( registry[module].style ) ) { - for ( var media in registry[module].style ) { - var style = registry[module].style[media]; - if ( $.isArray( style ) ) { - for ( var i = 0; i < style.length; i++ ) { - getMarker().before( mw.html.element( 'link', { - 'type': 'text/css', - 'media': media, - 'rel': 'stylesheet', - 'href': style[i] - } ) ); + + /** + * Executes a loaded module, making it ready to use + * + * @param module string module name to execute + */ + function execute( module, callback ) { + var style, media, i, script, markModuleReady, nestedAddScript; + + if ( registry[module] === undefined ) { + throw new Error( 'Module has not been registered yet: ' + module ); + } else if ( registry[module].state === 'registered' ) { + throw new Error( 'Module has not been requested from the server yet: ' + module ); + } else if ( registry[module].state === 'loading' ) { + throw new Error( 'Module has not completed loading yet: ' + module ); + } else if ( registry[module].state === 'ready' ) { + throw new Error( 'Module has already been loaded: ' + module ); + } + + // Add styles + if ( $.isPlainObject( registry[module].style ) ) { + for ( media in registry[module].style ) { + style = registry[module].style[media]; + if ( $.isArray( style ) ) { + for ( i = 0; i < style.length; i += 1 ) { + getMarker().before( mw.html.element( 'link', { + 'type': 'text/css', + 'media': media, + 'rel': 'stylesheet', + 'href': style[i] + } ) ); + } + } else if ( typeof style === 'string' ) { + addInlineCSS( style, media ); } - } else if ( typeof style === 'string' ) { - getMarker().before( mw.html.element( - 'style', - { 'type': 'text/css', 'media': media }, - new mw.html.Cdata( style ) - ) ); } } - } - // Add localizations to message system - if ( $.isPlainObject( registry[module].messages ) ) { - mw.messages.set( registry[module].messages ); - } - // Execute script - try { - var script = registry[module].script, + // Add localizations to message system + if ( $.isPlainObject( registry[module].messages ) ) { + mw.messages.set( registry[module].messages ); + } + // Execute script + try { + script = registry[module].script; markModuleReady = function() { registry[module].state = 'ready'; handlePending( module ); if ( $.isFunction( callback ) ) { callback(); } - }, - nestedAddScript = function( arr, callback, i ) { + }; + nestedAddScript = function ( arr, callback, async, i ) { // Recursively call addScript() in its own callback // for each element of arr. if ( i >= arr.length ) { @@ -536,666 +752,702 @@ window.mediaWiki = new ( function( $ ) { callback(); return; } - + addScript( arr[i], function() { - nestedAddScript( arr, callback, i + 1 ); - } ); + nestedAddScript( arr, callback, async, i + 1 ); + }, async ); }; - - if ( $.isArray( script ) ) { - registry[module].state = 'loading'; - nestedAddScript( script, markModuleReady, 0 ); - } else if ( $.isFunction( script ) ) { - script( jQuery ); - markModuleReady(); - } - } catch ( e ) { - // This needs to NOT use mw.log because these errors are common in production mode - // and not in debug mode, such as when a symbol that should be global isn't exported - if ( window.console && typeof window.console.log === 'function' ) { - console.log( _fn + 'Exception thrown by ' + module + ': ' + e.message ); + + if ( $.isArray( script ) ) { + registry[module].state = 'loading'; + nestedAddScript( script, markModuleReady, registry[module].async, 0 ); + } else if ( $.isFunction( script ) ) { + script( $ ); + markModuleReady(); + } + } catch ( e ) { + // This needs to NOT use mw.log because these errors are common in production mode + // and not in debug mode, such as when a symbol that should be global isn't exported + if ( window.console && typeof window.console.log === 'function' ) { + console.log( 'mw.loader::execute> Exception thrown by ' + module + ': ' + e.message ); + } + registry[module].state = 'error'; } - registry[module].state = 'error'; - throw e; } - } - - /** - * Automatically executes jobs and modules which are pending with satistifed dependencies. - * - * This is used when dependencies are satisfied, such as when a module is executed. - */ - function handlePending( module ) { - try { - // Run jobs who's dependencies have just been met - for ( var j = 0; j < jobs.length; j++ ) { - if ( compare( - filter( 'ready', jobs[j].dependencies ), - jobs[j].dependencies ) ) - { - if ( $.isFunction( jobs[j].ready ) ) { - jobs[j].ready(); + + /** + * Adds a dependencies to the queue with optional callbacks to be run + * when the dependencies are ready or fail + * + * @param dependencies string module name or array of string module names + * @param ready function callback to execute when all dependencies are ready + * @param error function callback to execute when any dependency fails + * @param async (optional) If true, load modules asynchronously even if + * document ready has not yet occurred + */ + function request( dependencies, ready, error, async ) { + var regItemDeps, regItemDepLen, n; + + // Allow calling by single module name + if ( typeof dependencies === 'string' ) { + dependencies = [dependencies]; + if ( registry[dependencies[0]] !== undefined ) { + // Cache repetitively accessed deep level object member + regItemDeps = registry[dependencies[0]].dependencies; + // Cache to avoid looped access to length property + regItemDepLen = regItemDeps.length; + for ( n = 0; n < regItemDepLen; n += 1 ) { + dependencies[dependencies.length] = regItemDeps[n]; } - jobs.splice( j, 1 ); - j--; } } - // Execute modules who's dependencies have just been met - for ( var r in registry ) { - if ( registry[r].state == 'loaded' ) { - if ( compare( - filter( ['ready'], registry[r].dependencies ), - registry[r].dependencies ) ) - { - execute( r ); - } - } + // Add ready and error callbacks if they were given + if ( arguments.length > 1 ) { + jobs[jobs.length] = { + 'dependencies': filter( + ['registered', 'loading', 'loaded'], + dependencies + ), + 'ready': ready, + 'error': error + }; } - } catch ( e ) { - // Run error callbacks of jobs affected by this condition - for ( var j = 0; j < jobs.length; j++ ) { - if ( $.inArray( module, jobs[j].dependencies ) !== -1 ) { - if ( $.isFunction( jobs[j].error ) ) { - jobs[j].error(); + // Queue up any dependencies that are registered + dependencies = filter( ['registered'], dependencies ); + for ( n = 0; n < dependencies.length; n += 1 ) { + if ( $.inArray( dependencies[n], queue ) === -1 ) { + queue[queue.length] = dependencies[n]; + if ( async ) { + // Mark this module as async in the registry + registry[dependencies[n]].async = true; } - jobs.splice( j, 1 ); - j--; } } + // Work the queue + mw.loader.work(); } - } - - /** - * Adds a dependencies to the queue with optional callbacks to be run - * when the dependencies are ready or fail - * - * @param dependencies string module name or array of string module names - * @param ready function callback to execute when all dependencies are ready - * @param error function callback to execute when any dependency fails - */ - function request( dependencies, ready, error ) { - // Allow calling by single module name - if ( typeof dependencies === 'string' ) { - dependencies = [dependencies]; - if ( dependencies[0] in registry ) { - for ( var n = 0; n < registry[dependencies[0]].dependencies.length; n++ ) { - dependencies[dependencies.length] = - registry[dependencies[0]].dependencies[n]; + + function sortQuery(o) { + var sorted = {}, key, a = []; + for ( key in o ) { + if ( hasOwn.call( o, key ) ) { + a.push( key ); } } - } - // Add ready and error callbacks if they were given - if ( arguments.length > 1 ) { - jobs[jobs.length] = { - 'dependencies': filter( - ['undefined', 'registered', 'loading', 'loaded'], - dependencies ), - 'ready': ready, - 'error': error - }; - } - // Queue up any dependencies that are undefined or registered - dependencies = filter( ['undefined', 'registered'], dependencies ); - for ( var n = 0; n < dependencies.length; n++ ) { - if ( $.inArray( dependencies[n], queue ) === -1 ) { - queue[queue.length] = dependencies[n]; + a.sort(); + for ( key = 0; key < a.length; key += 1 ) { + sorted[a[key]] = o[a[key]]; } + return sorted; } - // Work the queue - mw.loader.work(); - } - - function sortQuery(o) { - var sorted = {}, key, a = []; - for ( key in o ) { - if ( o.hasOwnProperty( key ) ) { - a.push( key ); + + /** + * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] } + * to a query string of the form foo.bar,baz|bar.baz,quux + */ + function buildModulesString( moduleMap ) { + var arr = [], p, prefix; + for ( prefix in moduleMap ) { + p = prefix === '' ? '' : prefix + '.'; + arr.push( p + moduleMap[prefix].join( ',' ) ); } + return arr.join( '|' ); } - a.sort(); - for ( key = 0; key < a.length; key++ ) { - sorted[a[key]] = o[a[key]]; - } - return sorted; - } - - /** - * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] } - * to a query string of the form foo.bar,baz|bar.baz,quux - */ - function buildModulesString( moduleMap ) { - var arr = []; - for ( var prefix in moduleMap ) { - var p = prefix === '' ? '' : prefix + '.'; - arr.push( p + moduleMap[prefix].join( ',' ) ); + + /** + * Asynchronously append a script tag to the end of the body + * that invokes load.php + * @param moduleMap {Object}: Module map, see buildModulesString() + * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request + * @param sourceLoadScript {String}: URL of load.php + * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred + */ + function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) { + var request = $.extend( + { 'modules': buildModulesString( moduleMap ) }, + currReqBase + ); + request = sortQuery( request ); + // Asynchronously append a script tag to the end of the body + // Append &* to avoid triggering the IE6 extension check + addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async ); } - return arr.join( '|' ); - } - - /** - * Adds a script tag to the body, either using document.write or low-level DOM manipulation, - * depending on whether document-ready has occured yet. - * - * @param src String: URL to script, will be used as the src attribute in the script tag - * @param callback Function: Optional callback which will be run when the script is done - */ - function addScript( src, callback ) { - if ( ready ) { - // jQuery's getScript method is NOT better than doing this the old-fashioned way - // because jQuery will eval the script's code, and errors will not have sane - // line numbers. - var script = document.createElement( 'script' ); - script.setAttribute( 'src', src ); - script.setAttribute( 'type', 'text/javascript' ); - if ( $.isFunction( callback ) ) { - var done = false; - // Attach handlers for all browsers -- this is based on jQuery.getScript - script.onload = script.onreadystatechange = function() { - if ( - !done - && ( - !this.readyState - || this.readyState === 'loaded' - || this.readyState === 'complete' - ) - ) { - done = true; - callback(); - // Handle memory leak in IE - script.onload = script.onreadystatechange = null; - if ( script.parentNode ) { - script.parentNode.removeChild( script ); + + /* Public Methods */ + return { + /** + * Requests dependencies from server, loading and executing when things when ready. + */ + work: function () { + var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup, + source, group, g, i, modules, maxVersion, sourceLoadScript, + currReqBase, currReqBaseLength, moduleMap, l, + lastDotIndex, prefix, suffix, bytesAdded, async; + + // Build a list of request parameters common to all requests. + reqBase = { + skin: mw.config.get( 'skin' ), + lang: mw.config.get( 'wgUserLanguage' ), + debug: mw.config.get( 'debug' ) + }; + // Split module batch by source and by group. + splits = {}; + maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 ); + + // Appends a list of modules from the queue to the batch + for ( q = 0; q < queue.length; q += 1 ) { + // Only request modules which are registered + if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) { + // Prevent duplicate entries + if ( $.inArray( queue[q], batch ) === -1 ) { + batch[batch.length] = queue[q]; + // Mark registered modules as loading + registry[queue[q]].state = 'loading'; + } + } + } + // Early exit if there's nothing to load... + if ( !batch.length ) { + return; + } + + // The queue has been processed into the batch, clear up the queue. + queue = []; + + // Always order modules alphabetically to help reduce cache + // misses for otherwise identical content. + batch.sort(); + + // Split batch by source and by group. + for ( b = 0; b < batch.length; b += 1 ) { + bSource = registry[batch[b]].source; + bGroup = registry[batch[b]].group; + if ( splits[bSource] === undefined ) { + splits[bSource] = {}; + } + if ( splits[bSource][bGroup] === undefined ) { + splits[bSource][bGroup] = []; + } + bSourceGroup = splits[bSource][bGroup]; + bSourceGroup[bSourceGroup.length] = batch[b]; + } + + // Clear the batch - this MUST happen before we append any + // script elements to the body or it's possible that a script + // will be locally cached, instantly load, and work the batch + // again, all before we've cleared it causing each request to + // include modules which are already loaded. + batch = []; + + for ( source in splits ) { + + sourceLoadScript = sources[source].loadScript; + + for ( group in splits[source] ) { + + // Cache access to currently selected list of + // modules for this group from this source. + modules = splits[source][group]; + + // Calculate the highest timestamp + maxVersion = 0; + for ( g = 0; g < modules.length; g += 1 ) { + if ( registry[modules[g]].version > maxVersion ) { + maxVersion = registry[modules[g]].version; + } + } + + currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase ); + currReqBaseLength = $.param( currReqBase ).length; + async = true; + // We may need to split up the request to honor the query string length limit, + // so build it piece by piece. + l = currReqBaseLength + 9; // '&modules='.length == 9 + + moduleMap = {}; // { prefix: [ suffixes ] } + + for ( i = 0; i < modules.length; i += 1 ) { + // Determine how many bytes this module would add to the query string + lastDotIndex = modules[i].lastIndexOf( '.' ); + // Note that these substr() calls work even if lastDotIndex == -1 + prefix = modules[i].substr( 0, lastDotIndex ); + suffix = modules[i].substr( lastDotIndex + 1 ); + bytesAdded = moduleMap[prefix] !== undefined + ? suffix.length + 3 // '%2C'.length == 3 + : modules[i].length + 3; // '%7C'.length == 3 + + // If the request would become too long, create a new one, + // but don't create empty requests + if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) { + // This request would become too long, create a new one + // and fire off the old one + doRequest( moduleMap, currReqBase, sourceLoadScript, async ); + moduleMap = {}; + async = true; + l = currReqBaseLength + 9; + } + if ( moduleMap[prefix] === undefined ) { + moduleMap[prefix] = []; + } + moduleMap[prefix].push( suffix ); + if ( !registry[modules[i]].async ) { + // If this module is blocking, make the entire request blocking + // This is slightly suboptimal, but in practice mixing of blocking + // and async modules will only occur in debug mode. + async = false; + } + l += bytesAdded; + } + // If there's anything left in moduleMap, request that too + if ( !$.isEmptyObject( moduleMap ) ) { + doRequest( moduleMap, currReqBase, sourceLoadScript, async ); } } + } + }, + + /** + * Register a source. + * + * @param id {String}: Short lowercase a-Z string representing a source, only used internally. + * @param props {Object}: Object containing only the loadScript property which is a url to + * the load.php location of the source. + * @return {Boolean} + */ + addSource: function ( id, props ) { + var source; + // Allow multiple additions + if ( typeof id === 'object' ) { + for ( source in id ) { + mw.loader.addSource( source, id[source] ); + } + return true; + } + + if ( sources[id] !== undefined ) { + throw new Error( 'source already registered: ' + id ); + } + + sources[id] = props; + + return true; + }, + + /** + * Registers a module, letting the system know about it and its + * properties. Startup modules contain calls to this function. + * + * @param module {String}: Module name + * @param version {Number}: Module version number as a timestamp (falls backs to 0) + * @param dependencies {String|Array|Function}: One string or array of strings of module + * names on which this module depends, or a function that returns that array. + * @param group {String}: Group which the module is in (optional, defaults to null) + * @param source {String}: Name of the source. Defaults to local. + */ + register: function ( module, version, dependencies, group, source ) { + var m; + // Allow multiple registration + if ( typeof module === 'object' ) { + for ( m = 0; m < module.length; m += 1 ) { + // module is an array of module names + if ( typeof module[m] === 'string' ) { + mw.loader.register( module[m] ); + // module is an array of arrays + } else if ( typeof module[m] === 'object' ) { + mw.loader.register.apply( mw.loader, module[m] ); + } + } + return; + } + // Validate input + if ( typeof module !== 'string' ) { + throw new Error( 'module must be a string, not a ' + typeof module ); + } + if ( registry[module] !== undefined ) { + throw new Error( 'module already registered: ' + module ); + } + // List the module as registered + registry[module] = { + 'version': version !== undefined ? parseInt( version, 10 ) : 0, + 'dependencies': [], + 'group': typeof group === 'string' ? group : null, + 'source': typeof source === 'string' ? source: 'local', + 'state': 'registered' }; - } - document.body.appendChild( script ); - } else { - document.write( mw.html.element( - 'script', { 'type': 'text/javascript', 'src': src }, '' - ) ); - if ( $.isFunction( callback ) ) { - // Document.write is synchronous, so this is called when it's done - callback(); - } - } - } - - /* Public Methods */ - - /** - * Requests dependencies from server, loading and executing when things when ready. - */ - this.work = function() { - // Appends a list of modules to the batch - for ( var q = 0; q < queue.length; q++ ) { - // Only request modules which are undefined or registered - if ( !( queue[q] in registry ) || registry[queue[q]].state == 'registered' ) { - // Prevent duplicate entries - if ( $.inArray( queue[q], batch ) === -1 ) { - batch[batch.length] = queue[q]; - // Mark registered modules as loading - if ( queue[q] in registry ) { - registry[queue[q]].state = 'loading'; + if ( typeof dependencies === 'string' ) { + // Allow dependencies to be given as a single module name + registry[module].dependencies = [dependencies]; + } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) { + // Allow dependencies to be given as an array of module names + // or a function which returns an array + registry[module].dependencies = dependencies; + } + }, + + /** + * Implements a module, giving the system a course of action to take + * upon loading. Results of a request for one or more modules contain + * calls to this function. + * + * All arguments are required. + * + * @param module String: Name of module + * @param script Mixed: Function of module code or String of URL to be used as the src + * attribute when adding a script element to the body + * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs + * keyed by media-type + * @param msgs Object: List of key/value pairs to be passed through mw.messages.set + */ + implement: function ( module, script, style, msgs ) { + // Validate input + if ( typeof module !== 'string' ) { + throw new Error( 'module must be a string, not a ' + typeof module ); + } + if ( !$.isFunction( script ) && !$.isArray( script ) ) { + throw new Error( 'script must be a function or an array, not a ' + typeof script ); + } + if ( !$.isPlainObject( style ) ) { + throw new Error( 'style must be an object, not a ' + typeof style ); + } + if ( !$.isPlainObject( msgs ) ) { + throw new Error( 'msgs must be an object, not a ' + typeof msgs ); + } + // Automatically register module + if ( registry[module] === undefined ) { + mw.loader.register( module ); + } + // Check for duplicate implementation + if ( registry[module] !== undefined && registry[module].script !== undefined ) { + throw new Error( 'module already implemented: ' + module ); + } + // Mark module as loaded + registry[module].state = 'loaded'; + // Attach components + registry[module].script = script; + registry[module].style = style; + registry[module].messages = msgs; + // Execute or queue callback + if ( compare( + filter( ['ready'], registry[module].dependencies ), + registry[module].dependencies ) ) + { + execute( module ); + } + }, + + /** + * Executes a function as soon as one or more required modules are ready + * + * @param dependencies {String|Array} Module name or array of modules names the callback + * dependends on to be ready before executing + * @param ready {Function} callback to execute when all dependencies are ready (optional) + * @param error {Function} callback to execute when if dependencies have a errors (optional) + */ + using: function ( dependencies, ready, error ) { + var tod = typeof dependencies; + // Validate input + if ( tod !== 'object' && tod !== 'string' ) { + throw new Error( 'dependencies must be a string or an array, not a ' + tod ); + } + // Allow calling with a single dependency as a string + if ( tod === 'string' ) { + dependencies = [dependencies]; + } + // Resolve entire dependency map + dependencies = resolve( dependencies ); + // If all dependencies are met, execute ready immediately + if ( compare( filter( ['ready'], dependencies ), dependencies ) ) { + if ( $.isFunction( ready ) ) { + ready(); } } - } - } - // Early exit if there's nothing to load - if ( !batch.length ) { - return; - } - // Clean up the queue - queue = []; - // Always order modules alphabetically to help reduce cache - // misses for otherwise identical content - batch.sort(); - // Build a list of request parameters - var base = { - 'skin': mw.config.get( 'skin' ), - 'lang': mw.config.get( 'wgUserLanguage' ), - 'debug': mw.config.get( 'debug' ) - }; - // Extend request parameters with a list of modules in the batch - var requests = []; - // Split into groups - var groups = {}; - for ( var b = 0; b < batch.length; b++ ) { - var group = registry[batch[b]].group; - if ( !( group in groups ) ) { - groups[group] = []; - } - groups[group][groups[group].length] = batch[b]; - } - for ( var group in groups ) { - // Calculate the highest timestamp - var version = 0; - for ( var g = 0; g < groups[group].length; g++ ) { - if ( registry[groups[group][g]].version > version ) { - version = registry[groups[group][g]].version; + // If any dependencies have errors execute error immediately + else if ( filter( ['error'], dependencies ).length ) { + if ( $.isFunction( error ) ) { + error( new Error( 'one or more dependencies have state "error"' ), + dependencies ); + } } - } - var reqBase = $.extend( { 'version': formatVersionNumber( version ) }, base ); - var reqBaseLength = $.param( reqBase ).length; - var reqs = []; - var limit = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 ); - // We may need to split up the request to honor the query string length limit - // So build it piece by piece - var l = reqBaseLength + 9; // '&modules='.length == 9 - var r = 0; - reqs[0] = {}; // { prefix: [ suffixes ] } - for ( var i = 0; i < groups[group].length; i++ ) { - // Determine how many bytes this module would add to the query string - var lastDotIndex = groups[group][i].lastIndexOf( '.' ); - // Note that these substr() calls work even if lastDotIndex == -1 - var prefix = groups[group][i].substr( 0, lastDotIndex ); - var suffix = groups[group][i].substr( lastDotIndex + 1 ); - var bytesAdded = prefix in reqs[r] ? - suffix.length + 3 : // '%2C'.length == 3 - groups[group][i].length + 3; // '%7C'.length == 3 - - // If the request would become too long, create a new one, - // but don't create empty requests - if ( limit > 0 && reqs[r] != {} && l + bytesAdded > limit ) { - // This request would become too long, create a new one - r++; - reqs[r] = {}; - l = reqBaseLength + 9; + // Since some dependencies are not yet ready, queue up a request + else { + request( dependencies, ready, error ); } - if ( !( prefix in reqs[r] ) ) { - reqs[r][prefix] = []; + }, + + /** + * Loads an external script or one or more modules for future use + * + * @param modules {mixed} Either the name of a module, array of modules, + * or a URL of an external script or style + * @param type {String} mime-type to use if calling with a URL of an + * external script or style; acceptable values are "text/css" and + * "text/javascript"; if no type is provided, text/javascript is assumed. + * @param async {Boolean} (optional) If true, load modules asynchronously + * even if document ready has not yet occurred. If false (default), + * block before document ready and load async after. If not set, true will + * be assumed if loading a URL, and false will be assumed otherwise. + */ + load: function ( modules, type, async ) { + var filtered, m; + + // Validate input + if ( typeof modules !== 'object' && typeof modules !== 'string' ) { + throw new Error( 'modules must be a string or an array, not a ' + typeof modules ); } - reqs[r][prefix].push( suffix ); - l += bytesAdded; - } - for ( var r = 0; r < reqs.length; r++ ) { - requests[requests.length] = $.extend( - { 'modules': buildModulesString( reqs[r] ) }, reqBase - ); - } - } - // Clear the batch - this MUST happen before we append the - // script element to the body or it's possible that the script - // will be locally cached, instantly load, and work the batch - // again, all before we've cleared it causing each request to - // include modules which are already loaded - batch = []; - // Asynchronously append a script tag to the end of the body - for ( var r = 0; r < requests.length; r++ ) { - requests[r] = sortQuery( requests[r] ); - // Append &* to avoid triggering the IE6 extension check - var src = mw.config.get( 'wgLoadScript' ) + '?' + $.param( requests[r] ) + '&*'; - addScript( src ); - } - }; - - /** - * Registers a module, letting the system know about it and its - * dependencies. loader.js files contain calls to this function. - */ - this.register = function( module, version, dependencies, group ) { - // Allow multiple registration - if ( typeof module === 'object' ) { - for ( var m = 0; m < module.length; m++ ) { - if ( typeof module[m] === 'string' ) { - mw.loader.register( module[m] ); - } else if ( typeof module[m] === 'object' ) { - mw.loader.register.apply( mw.loader, module[m] ); + // Allow calling with an external url or single dependency as a string + if ( typeof modules === 'string' ) { + // Support adding arbitrary external scripts + if ( /^(https?:)?\/\//.test( modules ) ) { + if ( async === undefined ) { + // Assume async for bug 34542 + async = true; + } + if ( type === 'text/css' ) { + $( 'head' ).append( $( '<link>', { + rel: 'stylesheet', + type: 'text/css', + href: modules + } ) ); + return; + } else if ( type === 'text/javascript' || type === undefined ) { + addScript( modules, null, async ); + return; + } + // Unknown type + throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type ); + } + // Called with single module + modules = [modules]; } - } - return; - } - // Validate input - if ( typeof module !== 'string' ) { - throw new Error( 'module must be a string, not a ' + typeof module ); - } - if ( registry[module] !== undefined ) { - throw new Error( 'module already implemented: ' + module ); - } - // List the module as registered - registry[module] = { - 'state': 'registered', - 'group': typeof group === 'string' ? group : null, - 'dependencies': [], - 'version': version !== undefined ? parseInt( version, 10 ) : 0 - }; - if ( typeof dependencies === 'string' ) { - // Allow dependencies to be given as a single module name - registry[module].dependencies = [dependencies]; - } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) { - // Allow dependencies to be given as an array of module names - // or a function which returns an array - registry[module].dependencies = dependencies; - } - }; - /** - * Implements a module, giving the system a course of action to take - * upon loading. Results of a request for one or more modules contain - * calls to this function. - * - * All arguments are required. - * - * @param module String: Name of module - * @param script Mixed: Function of module code or String of URL to be used as the src - * attribute when adding a script element to the body - * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs - * keyed by media-type - * @param msgs Object: List of key/value pairs to be passed through mw.messages.set - */ - this.implement = function( module, script, style, msgs ) { - // Validate input - if ( typeof module !== 'string' ) { - throw new Error( 'module must be a string, not a ' + typeof module ); - } - if ( !$.isFunction( script ) && !$.isArray( script ) ) { - throw new Error( 'script must be a function or an array, not a ' + typeof script ); - } - if ( !$.isPlainObject( style ) ) { - throw new Error( 'style must be an object, not a ' + typeof style ); - } - if ( !$.isPlainObject( msgs ) ) { - throw new Error( 'msgs must be an object, not a ' + typeof msgs ); - } - // Automatically register module - if ( registry[module] === undefined ) { - mw.loader.register( module ); - } - // Check for duplicate implementation - if ( registry[module] !== undefined && registry[module].script !== undefined ) { - throw new Error( 'module already implemeneted: ' + module ); - } - // Mark module as loaded - registry[module].state = 'loaded'; - // Attach components - registry[module].script = script; - registry[module].style = style; - registry[module].messages = msgs; - // Execute or queue callback - if ( compare( - filter( ['ready'], registry[module].dependencies ), - registry[module].dependencies ) ) - { - execute( module ); - } else { - request( module ); - } - }; - - /** - * Executes a function as soon as one or more required modules are ready - * - * @param dependencies string or array of strings of modules names the callback - * dependencies to be ready before - * executing - * @param ready function callback to execute when all dependencies are ready (optional) - * @param error function callback to execute when if dependencies have a errors (optional) - */ - this.using = function( dependencies, ready, error ) { - // Validate input - if ( typeof dependencies !== 'object' && typeof dependencies !== 'string' ) { - throw new Error( 'dependencies must be a string or an array, not a ' + - typeof dependencies ); - } - // Allow calling with a single dependency as a string - if ( typeof dependencies === 'string' ) { - dependencies = [dependencies]; - } - // Resolve entire dependency map - dependencies = resolve( dependencies ); - // If all dependencies are met, execute ready immediately - if ( compare( filter( ['ready'], dependencies ), dependencies ) ) { - if ( $.isFunction( ready ) ) { - ready(); - } - } - // If any dependencies have errors execute error immediately - else if ( filter( ['error'], dependencies ).length ) { - if ( $.isFunction( error ) ) { - error(); - } - } - // Since some dependencies are not yet ready, queue up a request - else { - request( dependencies, ready, error ); - } - }; + // Filter out undefined modules, otherwise resolve() will throw + // an exception for trying to load an undefined module. + // Undefined modules are acceptable here in load(), because load() takes + // an array of unrelated modules, whereas the modules passed to + // using() are related and must all be loaded. + for ( filtered = [], m = 0; m < modules.length; m += 1 ) { + if ( registry[modules[m]] !== undefined ) { + filtered[filtered.length] = modules[m]; + } + } - /** - * Loads an external script or one or more modules for future use - * - * @param modules mixed either the name of a module, array of modules, - * or a URL of an external script or style - * @param type string mime-type to use if calling with a URL of an - * external script or style; acceptable values are "text/css" and - * "text/javascript"; if no type is provided, text/javascript is - * assumed - */ - this.load = function( modules, type ) { - // Validate input - if ( typeof modules !== 'object' && typeof modules !== 'string' ) { - throw new Error( 'modules must be a string or an array, not a ' + - typeof modules ); - } - // Allow calling with an external script or single dependency as a string - if ( typeof modules === 'string' ) { - // Support adding arbitrary external scripts - if ( modules.substr( 0, 7 ) === 'http://' || modules.substr( 0, 8 ) === 'https://' || modules.substr( 0, 2 ) === '//' ) { - if ( type === 'text/css' ) { - $( 'head' ).append( $( '<link />', { - rel: 'stylesheet', - type: 'text/css', - href: modules - } ) ); - return true; - } else if ( type === 'text/javascript' || type === undefined ) { - addScript( modules ); - return true; + // Resolve entire dependency map + filtered = resolve( filtered ); + // If all modules are ready, nothing dependency be done + if ( compare( filter( ['ready'], filtered ), filtered ) ) { + return; } - // Unknown type - return false; + // If any modules have errors + else if ( filter( ['error'], filtered ).length ) { + return; + } + // Since some modules are not yet ready, queue up a request + else { + request( filtered, null, null, async ); + return; + } + }, + + /** + * Changes the state of a module + * + * @param module {String|Object} module name or object of module name/state pairs + * @param state {String} state name + */ + state: function ( module, state ) { + var m; + if ( typeof module === 'object' ) { + for ( m in module ) { + mw.loader.state( m, module[m] ); + } + return; + } + if ( registry[module] === undefined ) { + mw.loader.register( module ); + } + registry[module].state = state; + }, + + /** + * Gets the version of a module + * + * @param module string name of module to get version for + */ + getVersion: function ( module ) { + if ( registry[module] !== undefined && registry[module].version !== undefined ) { + return formatVersionNumber( registry[module].version ); + } + return null; + }, + + /** + * @deprecated since 1.18 use mw.loader.getVersion() instead + */ + version: function () { + return mw.loader.getVersion.apply( mw.loader, arguments ); + }, + + /** + * Gets the state of a module + * + * @param module string name of module to get state for + */ + getState: function ( module ) { + if ( registry[module] !== undefined && registry[module].state !== undefined ) { + return registry[module].state; + } + return null; + }, + + /** + * Get names of all registered modules. + * + * @return {Array} + */ + getModuleNames: function () { + return $.map( registry, function ( i, key ) { + return key; + } ); + }, + + /** + * For backwards-compatibility with Squid-cached pages. Loads mw.user + */ + go: function () { + mw.loader.load( 'mediawiki.user' ); } - // Called with single module - modules = [modules]; - } - // Resolve entire dependency map - modules = resolve( modules ); - // If all modules are ready, nothing dependency be done - if ( compare( filter( ['ready'], modules ), modules ) ) { - return true; - } - // If any modules have errors return false - else if ( filter( ['error'], modules ).length ) { - return false; - } - // Since some modules are not yet ready, queue up a request - else { - request( modules ); - return true; - } - }; - - /** - * Changes the state of a module - * - * @param module string module name or object of module name/state pairs - * @param state string state name - */ - this.state = function( module, state ) { - if ( typeof module === 'object' ) { - for ( var m in module ) { - mw.loader.state( m, module[m] ); + }; + }() ), + + /** HTML construction helper functions */ + html: ( function () { + function escapeCallback( s ) { + switch ( s ) { + case "'": + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; } - return; } - if ( !( module in registry ) ) { - mw.loader.register( module ); - } - registry[module].state = state; - }; - - /** - * Gets the version of a module - * - * @param module string name of module to get version for - */ - this.getVersion = function( module ) { - if ( module in registry && 'version' in registry[module] ) { - return formatVersionNumber( registry[module].version ); - } - return null; - }; - /** - * @deprecated use mw.loader.getVersion() instead - */ - this.version = function() { - return mediaWiki.loader.getVersion.apply( mediaWiki.loader, arguments ); - }; - /** - * Gets the state of a module - * - * @param module string name of module to get state for - */ - this.getState = function( module ) { - if ( module in registry && 'state' in registry[module] ) { - return registry[module].state; - } - return null; - }; + return { + /** + * Escape a string for HTML. Converts special characters to HTML entities. + * @param s The string to escape + */ + escape: function ( s ) { + return s.replace( /['"<>&]/g, escapeCallback ); + }, - /** - * For backwards-compatibility with Squid-cached pages. Loads mw.user - */ - this.go = function() { mw.loader.load( 'mediawiki.user' ); }; - - /* Cache document ready status */ - - $(document).ready( function() { ready = true; } ); - } )(); - - /** HTML construction helper functions */ - this.html = new ( function () { - var escapeCallback = function( s ) { - switch ( s ) { - case "'": - return '''; - case '"': - return '"'; - case '<': - return '<'; - case '>': - return '>'; - case '&': - return '&'; - } - }; - - /** - * Escape a string for HTML. Converts special characters to HTML entities. - * @param s The string to escape - */ - this.escape = function( s ) { - return s.replace( /['"<>&]/g, escapeCallback ); - }; - - /** - * Wrapper object for raw HTML passed to mw.html.element(). - */ - this.Raw = function( value ) { - this.value = value; - }; - - /** - * Wrapper object for CDATA element contents passed to mw.html.element() - */ - this.Cdata = function( value ) { - this.value = value; - }; - - /** - * Create an HTML element string, with safe escaping. - * - * @param name The tag name. - * @param attrs An object with members mapping element names to values - * @param contents The contents of the element. May be either: - * - string: The string is escaped. - * - null or undefined: The short closing form is used, e.g. <br/>. - * - this.Raw: The value attribute is included without escaping. - * - this.Cdata: The value attribute is included, and an exception is - * thrown if it contains an illegal ETAGO delimiter. - * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2 - * - * Example: - * var h = mw.html; - * return h.element( 'div', {}, - * new h.Raw( h.element( 'img', {src: '<'} ) ) ); - * Returns <div><img src="<"/></div> - */ - this.element = function( name, attrs, contents ) { - var v, s = '<' + name; - for ( var attrName in attrs ) { - v = attrs[attrName]; - // Convert name=true, to name=name - if ( v === true ) { - v = attrName; - // Skip name=false - } else if ( v === false ) { - continue; - } - s += ' ' + attrName + '="' + this.escape( '' + v ) + '"'; - } - if ( contents === undefined || contents === null ) { - // Self close tag - s += '/>'; - return s; - } - // Regular open tag - s += '>'; - switch ( typeof contents ) { - case 'string': - // Escaped - s += this.escape( contents ); - break; - case 'number': - case 'boolean': - // Convert to string - s += '' + contents; - break; - default: - if ( contents instanceof this.Raw ) { - // Raw HTML inclusion - s += contents.value; - } else if ( contents instanceof this.Cdata ) { - // CDATA - if ( /<\/[a-zA-z]/.test( contents.value ) ) { - throw new Error( 'mw.html.element: Illegal end tag found in CDATA' ); + /** + * Wrapper object for raw HTML passed to mw.html.element(). + * @constructor + */ + Raw: function ( value ) { + this.value = value; + }, + + /** + * Wrapper object for CDATA element contents passed to mw.html.element() + * @constructor + */ + Cdata: function ( value ) { + this.value = value; + }, + + /** + * Create an HTML element string, with safe escaping. + * + * @param name The tag name. + * @param attrs An object with members mapping element names to values + * @param contents The contents of the element. May be either: + * - string: The string is escaped. + * - null or undefined: The short closing form is used, e.g. <br/>. + * - this.Raw: The value attribute is included without escaping. + * - this.Cdata: The value attribute is included, and an exception is + * thrown if it contains an illegal ETAGO delimiter. + * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2 + * + * Example: + * var h = mw.html; + * return h.element( 'div', {}, + * new h.Raw( h.element( 'img', {src: '<'} ) ) ); + * Returns <div><img src="<"/></div> + */ + element: function ( name, attrs, contents ) { + var v, attrName, s = '<' + name; + + for ( attrName in attrs ) { + v = attrs[attrName]; + // Convert name=true, to name=name + if ( v === true ) { + v = attrName; + // Skip name=false + } else if ( v === false ) { + continue; } - s += contents.value; - } else { - throw new Error( 'mw.html.element: Invalid type of contents' ); + s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"'; } - } - s += '</' + name + '>'; - return s; - }; - } )(); - - /* Extension points */ - - this.legacy = {}; + if ( contents === undefined || contents === null ) { + // Self close tag + s += '/>'; + return s; + } + // Regular open tag + s += '>'; + switch ( typeof contents ) { + case 'string': + // Escaped + s += this.escape( contents ); + break; + case 'number': + case 'boolean': + // Convert to string + s += String( contents ); + break; + default: + if ( contents instanceof this.Raw ) { + // Raw HTML inclusion + s += contents.value; + } else if ( contents instanceof this.Cdata ) { + // CDATA + if ( /<\/[a-zA-z]/.test( contents.value ) ) { + throw new Error( 'mw.html.element: Illegal end tag found in CDATA' ); + } + s += contents.value; + } else { + throw new Error( 'mw.html.element: Invalid type of contents' ); + } + } + s += '</' + name + '>'; + return s; + } + }; + })(), -} )( jQuery ); + // Skeleton user object. mediawiki.user.js extends this + user: { + options: new Map(), + tokens: new Map() + } + }; + +})( jQuery ); // Alias $j to jQuery for backwards compatibility window.$j = jQuery; -// Global alias -window.mw = mediaWiki; - -/* Auto-register from pre-loaded startup scripts */ +// Attach to window and globally alias +window.mw = window.mediaWiki = mw; -if ( jQuery.isFunction( startUp ) ) { +// Auto-register from pre-loaded startup scripts +if ( typeof startUp !== 'undefined' && jQuery.isFunction( startUp ) ) { startUp(); - delete startUp; + startUp = undefined; } diff --git a/resources/mediawiki/mediawiki.log.js b/resources/mediawiki/mediawiki.log.js index 38f3411f..ad4c73df 100644 --- a/resources/mediawiki/mediawiki.log.js +++ b/resources/mediawiki/mediawiki.log.js @@ -1,67 +1,70 @@ -/* - * Implementation for mediaWiki.log stub +/** + * Logger for MediaWiki javascript. + * Implements the stub left by the main 'mediawiki' module. + * + * @author Michael Dale <mdale@wikimedia.org> + * @author Trevor Parscal <tparscal@wikimedia.org> */ -(function( $ ) { +( function ( $ ) { /** - * Log output to the console. + * Logs a message to the console. * - * In the case that the browser does not have a console available, one is created by appending a - * <div> element to the bottom of the body and then appending a <div> element to that for each - * message. + * In the case the browser does not have a console API, a console is created on-the-fly by appending + * a <div id="mw-log-console"> element to the bottom of the body and then appending this and future + * messages to that, instead of the console. * - * @author Michael Dale <mdale@wikimedia.org> - * @author Trevor Parscal <tparscal@wikimedia.org> - * @param logmsg string Message to output to console. + * @param {String} First in list of variadic messages to output to console. */ - mw.log = function( logmsg ) { - // Allow log messages to use a configured prefix to identify the source window (ie. frame) - if ( mw.config.exists( 'mw.log.prefix' ) ) { - logmsg = mw.config.get( 'mw.log.prefix' ) + '> ' + logmsg; - } + mw.log = function( /* logmsg, logmsg, */ ) { + // Turn arguments into an array + var args = Array.prototype.slice.call( arguments ), + // Allow log messages to use a configured prefix to identify the source window (ie. frame) + prefix = mw.config.exists( 'mw.log.prefix' ) ? mw.config.get( 'mw.log.prefix' ) + '> ' : ''; + // Try to use an existing console if ( window.console !== undefined && $.isFunction( window.console.log ) ) { - window.console.log( logmsg ); - } else { - // Set timestamp - var d = new Date(); - var time = ( d.getHours() < 10 ? '0' + d.getHours() : d.getHours() ) + + args.unshift( prefix ); + window.console.log.apply( window.console, args ); + return; + } + + // If there is no console, use our own log box + mw.loader.using( 'jquery.footHovzer', function () { + + var d = new Date(), + // Create HH:MM:SS.MIL timestamp + time = ( d.getHours() < 10 ? '0' + d.getHours() : d.getHours() ) + ':' + ( d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes() ) + ':' + ( d.getSeconds() < 10 ? '0' + d.getSeconds() : d.getSeconds() ) + - '.' + ( d.getMilliseconds() < 10 ? '00' + d.getMilliseconds() : ( d.getMilliseconds() < 100 ? '0' + d.getMilliseconds() : d.getMilliseconds() ) ); - // Show a log box for console-less browsers - var $log = $( '#mw-log-console' ); + '.' + ( d.getMilliseconds() < 10 ? '00' + d.getMilliseconds() : ( d.getMilliseconds() < 100 ? '0' + d.getMilliseconds() : d.getMilliseconds() ) ), + $log = $( '#mw-log-console' ); + if ( !$log.length ) { - $log = $( '<div id="mw-log-console"></div>' ) - .css( { - 'position': 'fixed', - 'overflow': 'auto', - 'z-index': 500, - 'bottom': '0px', - 'left': '0px', - 'right': '0px', - 'height': '150px', - 'background-color': 'white', - 'border-top': 'solid 2px #ADADAD' + $log = $( '<div id="mw-log-console"></div>' ).css( { + overflow: 'auto', + height: '150px', + backgroundColor: 'white', + borderTop: 'solid 2px #ADADAD' } ); - $( 'body' ) - .css( 'padding-bottom', '150px' ) // don't hide anything - .append( $log ); + var hovzer = $.getFootHovzer(); + hovzer.$.append( $log ); + hovzer.update(); } $log.append( $( '<div></div>' ) .css( { - 'border-bottom': 'solid 1px #DDDDDD', - 'font-size': 'small', - 'font-family': 'monospace', - 'white-space': 'pre-wrap', - 'padding': '0.125em 0.25em' + borderBottom: 'solid 1px #DDDDDD', + fontSize: 'small', + fontFamily: 'monospace', + whiteSpace: 'pre-wrap', + padding: '0.125em 0.25em' } ) - .text( logmsg ) - .prepend( '<span style="float:right">[' + time + ']</span>' ) + .text( prefix + args.join( ', ' ) ) + .prepend( '<span style="float: right;">[' + time + ']</span>' ) ); - } + } ); }; -})(jQuery); +})( jQuery ); diff --git a/resources/mediawiki/mediawiki.user.js b/resources/mediawiki/mediawiki.user.js index b0176cf4..7f881b0e 100644 --- a/resources/mediawiki/mediawiki.user.js +++ b/resources/mediawiki/mediawiki.user.js @@ -1,5 +1,5 @@ /* - * Implementation for mediaWiki.log stub + * Implementation for mediaWiki.user */ (function( $ ) { @@ -7,7 +7,7 @@ /** * User object */ - function User() { + function User( options, tokens ) { /* Private Members */ @@ -15,9 +15,9 @@ /* Public Members */ - this.options = new mw.Map(); + this.options = options || new mw.Map(); - this.tokens = new mw.Map(); + this.tokens = tokens || new mw.Map(); /* Public Methods */ @@ -176,6 +176,8 @@ }; } - mw.user = new User(); + // Extend the skeleton mw.user from mediawiki.js + // This is kind of ugly but we're stuck with this for b/c reasons + mw.user = new User( mw.user.options, mw.user.tokens ); })(jQuery);
\ No newline at end of file diff --git a/resources/mediawiki/mediawiki.util.js b/resources/mediawiki/mediawiki.util.js index 59727b3d..0a95d102 100644 --- a/resources/mediawiki/mediawiki.util.js +++ b/resources/mediawiki/mediawiki.util.js @@ -1,109 +1,112 @@ /** - * Utilities + * Implements mediaWiki.util library */ -( function( $ ) { +( function ( $, mw ) { + "use strict"; // Local cache and alias - var util = mw.util = { + var util = { - /* Initialisation */ /** - * @var boolean Wether or not already initialised + * Initialisation + * (don't call before document ready) */ - 'initialised' : false, - 'init' : function() { - if ( this.initialised === false ) { - this.initialised = true; - - // Folllowing the initialisation after the DOM is ready - $(document).ready( function() { - - /* Set up $.messageBox */ - $.messageBoxNew( { - 'id': 'mw-js-message', - 'parent': '#content' - } ); + init: function () { + var profile, $tocTitle, $tocToggleLink, hideTocCookie; - // Shortcut to client profile return - var profile = $.client.profile(); - - /* Set tooltipAccessKeyPrefix */ - - // Opera on any platform - if ( profile.name == 'opera' ) { - util.tooltipAccessKeyPrefix = 'shift-esc-'; - - // Chrome on any platform - } else if ( profile.name == 'chrome' ) { - // Chrome on Mac or Chrome on other platform ? - util.tooltipAccessKeyPrefix = ( profile.platform == 'mac' - ? 'ctrl-option-' : 'alt-' ); - - // Non-Windows Safari with webkit_version > 526 - } else if ( profile.platform !== 'win' - && profile.name == 'safari' - && profile.layoutVersion > 526 ) { - util.tooltipAccessKeyPrefix = 'ctrl-alt-'; - - // Safari/Konqueror on any platform, or any browser on Mac - // (but not Safari on Windows) - } else if ( !( profile.platform == 'win' && profile.name == 'safari' ) - && ( profile.name == 'safari' - || profile.platform == 'mac' - || profile.name == 'konqueror' ) ) { - util.tooltipAccessKeyPrefix = 'ctrl-'; - - // Firefox 2.x and later - } else if ( profile.name == 'firefox' && profile.versionBase > '1' ) { - util.tooltipAccessKeyPrefix = 'alt-shift-'; - } + /* Set up $.messageBox */ + $.messageBoxNew( { + id: 'mw-js-message', + parent: '#content' + } ); - /* Fill $content var */ - if ( $( '#bodyContent' ).length ) { - // Vector, Monobook, Chick etc. - util.$content = $( '#bodyContent' ); + /* Set tooltipAccessKeyPrefix */ + profile = $.client.profile(); + + // Opera on any platform + if ( profile.name === 'opera' ) { + util.tooltipAccessKeyPrefix = 'shift-esc-'; + + // Chrome on any platform + } else if ( profile.name === 'chrome' ) { + + util.tooltipAccessKeyPrefix = ( + profile.platform === 'mac' + // Chrome on Mac + ? 'ctrl-option-' + : profile.platform === 'win' + // Chrome on Windows + // (both alt- and alt-shift work, but alt-f triggers Chrome wrench menu + // which alt-shift-f does not) + ? 'alt-shift-' + // Chrome on other (Ubuntu?) + : 'alt-' + ); - } else if ( $( '#mw_contentholder' ).length ) { - // Modern - util.$content = $( '#mw_contentholder' ); + // Non-Windows Safari with webkit_version > 526 + } else if ( profile.platform !== 'win' + && profile.name === 'safari' + && profile.layoutVersion > 526 ) { + util.tooltipAccessKeyPrefix = 'ctrl-alt-'; + + // Safari/Konqueror on any platform, or any browser on Mac + // (but not Safari on Windows) + } else if ( !( profile.platform === 'win' && profile.name === 'safari' ) + && ( profile.name === 'safari' + || profile.platform === 'mac' + || profile.name === 'konqueror' ) ) { + util.tooltipAccessKeyPrefix = 'ctrl-'; + + // Firefox 2.x and later + } else if ( profile.name === 'firefox' && profile.versionBase > '1' ) { + util.tooltipAccessKeyPrefix = 'alt-shift-'; + } - } else if ( $( '#article' ).length ) { - // Standard, CologneBlue - util.$content = $( '#article' ); + /* Fill $content var */ + if ( $( '#bodyContent' ).length ) { + // Vector, Monobook, Chick etc. + util.$content = $( '#bodyContent' ); - } else { - // #content is present on almost all if not all skins. Most skins (the above cases) - // have #content too, but as an outer wrapper instead of the article text container. - // The skins that don't have an outer wrapper do have #content for everything - // so it's a good fallback - util.$content = $( '#content' ); - } + } else if ( $( '#mw_contentholder' ).length ) { + // Modern + util.$content = $( '#mw_contentholder' ); - /* Table of Contents toggle */ - var $tocContainer = $( '#toc' ), - $tocTitle = $( '#toctitle' ), - $tocToggleLink = $( '#togglelink' ); - // Only add it if there is a TOC and there is no toggle added already - if ( $tocContainer.size() && $tocTitle.size() && !$tocToggleLink.size() ) { - var hideTocCookie = $.cookie( 'mw_hidetoc' ); - $tocToggleLink = $( '<a href="#" class="internal" id="togglelink"></a>' ) - .text( mw.msg( 'hidetoc' ) ) - .click( function(e){ - e.preventDefault(); - util.toggleToc( $(this) ); - } ); - $tocTitle.append( $tocToggleLink.wrap( '<span class="toctoggle"></span>' ).parent().prepend( ' [' ).append( '] ' ) ); - - if ( hideTocCookie == '1' ) { - // Cookie says user want toc hidden - $tocToggleLink.click(); - } - } - } ); + } else if ( $( '#article' ).length ) { + // Standard, CologneBlue + util.$content = $( '#article' ); - return true; + } else { + // #content is present on almost all if not all skins. Most skins (the above cases) + // have #content too, but as an outer wrapper instead of the article text container. + // The skins that don't have an outer wrapper do have #content for everything + // so it's a good fallback + util.$content = $( '#content' ); + } + + // Table of contents toggle + $tocTitle = $( '#toctitle' ); + $tocToggleLink = $( '#togglelink' ); + // Only add it if there is a TOC and there is no toggle added already + if ( $( '#toc' ).length && $tocTitle.length && !$tocToggleLink.length ) { + hideTocCookie = $.cookie( 'mw_hidetoc' ); + $tocToggleLink = $( '<a href="#" class="internal" id="togglelink"></a>' ) + .text( mw.msg( 'hidetoc' ) ) + .click( function ( e ) { + e.preventDefault(); + util.toggleToc( $(this) ); + } ); + $tocTitle.append( + $tocToggleLink + .wrap( '<span class="toctoggle"></span>' ) + .parent() + .prepend( ' [' ) + .append( '] ' ) + ); + + if ( hideTocCookie === '1' ) { + util.toggleToc( $tocToggleLink ); + } } - return false; }, /* Main body */ @@ -113,8 +116,8 @@ * * @param str string String to be encoded */ - 'rawurlencode' : function( str ) { - str = ( str + '' ).toString(); + rawurlencode: function ( str ) { + str = String( str ); return encodeURIComponent( str ) .replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' ) .replace( /\)/g, '%29' ).replace( /\*/g, '%2A' ).replace( /~/g, '%7E' ); @@ -127,20 +130,20 @@ * * @param str string String to be encoded */ - 'wikiUrlencode' : function( str ) { - return this.rawurlencode( str ) + wikiUrlencode: function ( str ) { + return util.rawurlencode( str ) .replace( /%20/g, '_' ).replace( /%3A/g, ':' ).replace( /%2F/g, '/' ); }, /** * Get the link to a page name (relative to wgServer) * - * @param str string Page name to get the link for. - * @return string Location for a page with name of 'str' or boolean false on error. + * @param str String: Page name to get the link for. + * @return String: Location for a page with name of 'str' or boolean false on error. */ - 'wikiGetlink' : function( str ) { + wikiGetlink: function ( str ) { return mw.config.get( 'wgArticlePath' ).replace( '$1', - this.wikiUrlencode( str || mw.config.get( 'wgPageName' ) ) ); + util.wikiUrlencode( typeof str === 'string' ? str : mw.config.get( 'wgPageName' ) ) ); }, /** @@ -150,8 +153,9 @@ * @param str string Name of script (eg. 'api'), defaults to 'index' * @return string Address to script (eg. '/w/api.php' ) */ - 'wikiScript' : function( str ) { - return mw.config.get( 'wgScriptPath' ) + '/' + ( str || 'index' ) + mw.config.get( 'wgScriptExtension' ); + wikiScript: function ( str ) { + return mw.config.get( 'wgScriptPath' ) + '/' + ( str || 'index' ) + + mw.config.get( 'wgScriptExtension' ); }, /** @@ -160,7 +164,7 @@ * @param text string CSS to be appended * @return CSSStyleSheet */ - 'addCSS' : function( text ) { + addCSS: function ( text ) { var s = document.createElement( 'style' ); s.type = 'text/css'; s.rel = 'stylesheet'; @@ -169,7 +173,8 @@ if ( s.styleSheet ) { s.styleSheet.cssText = text; // IE } else { - s.appendChild( document.createTextNode( text + '' ) ); // Safari sometimes borks on null + // Safari sometimes borks on null + s.appendChild( document.createTextNode( String( text ) ) ); } return s.sheet || s; }, @@ -183,12 +188,12 @@ * @return mixed Boolean visibility of the toc (true if it's visible) * or Null if there was no table of contents. */ - 'toggleToc' : function( $toggleLink, callback ) { + toggleToc: function ( $toggleLink, callback ) { var $tocList = $( '#toc ul:first' ); // This function shouldn't be called if there's no TOC, // but just in case... - if ( $tocList.size() ) { + if ( $tocList.length ) { if ( $tocList.is( ':hidden' ) ) { $tocList.slideDown( 'fast', callback ); $toggleLink.text( mw.msg( 'hidetoc' ) ); @@ -221,11 +226,11 @@ * @param url string URL to search through (optional) * @return mixed Parameter value or null. */ - 'getParamValue' : function( param, url ) { - url = url ? url : document.location.href; + getParamValue: function ( param, url ) { + url = url || document.location.href; // Get last match, stop at hash - var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ); - var m = re.exec( url ); + var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ), + m = re.exec( url ); if ( m && m.length > 1 ) { // Beware that decodeURIComponent is not required to understand '+' // by spec, as encodeURIComponent does not produce it. @@ -239,13 +244,13 @@ * Access key prefix. Will be re-defined based on browser/operating system * detection in mw.util.init(). */ - 'tooltipAccessKeyPrefix' : 'alt-', + tooltipAccessKeyPrefix: 'alt-', /** * @var RegExp * Regex to match accesskey tooltips. */ - 'tooltipAccessKeyRegexp': /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/, + tooltipAccessKeyRegexp: /\[(ctrl-)?(alt-)?(shift-)?(esc-)?(.)\]$/, /** * Add the appropriate prefix to the accesskey shown in the tooltip. @@ -253,36 +258,25 @@ * otherwise, all the nodes that will probably have accesskeys by * default are updated. * - * @param nodeList {Array|jQuery} (optional) A jQuery object, or array of elements to update. + * @param $nodes {Array|jQuery} [optional] A jQuery object, or array + * of elements to update. */ - 'updateTooltipAccessKeys' : function( nodeList ) { - var $nodes; - if ( !nodeList ) { - - // Rather than scanning all links, just the elements that - // contain the relevant links - this.updateTooltipAccessKeys( - $( '#column-one a, #mw-head a, #mw-panel a, #p-logo a' ) ); - - // these are rare enough that no such optimization is needed - this.updateTooltipAccessKeys( $( 'input' ) ); - this.updateTooltipAccessKeys( $( 'label' ) ); - - return; - - } else if ( nodeList instanceof jQuery ) { - $nodes = nodeList; - } else { - $nodes = $( nodeList ); + updateTooltipAccessKeys: function ( $nodes ) { + if ( !$nodes ) { + // Rather than going into a loop of all anchor tags, limit to few elements that + // contain the relevant anchor tags. + // Input and label are rare enough that no such optimization is needed + $nodes = $( '#column-one a, #mw-head a, #mw-panel a, #p-logo a, input, label' ); + } else if ( !( $nodes instanceof $ ) ) { + $nodes = $( $nodes ); } - $nodes.each( function ( i ) { - var tip = $(this).attr( 'title' ); - if ( !!tip && util.tooltipAccessKeyRegexp.exec( tip ) ) { - tip = tip.replace( util.tooltipAccessKeyRegexp, - '[' + util.tooltipAccessKeyPrefix + "$5]" ); - $(this).attr( 'title', tip ); + $nodes.attr( 'title', function ( i, val ) { + if ( val && util.tooltipAccessKeyRegexp.exec( val ) ) { + return val.replace( util.tooltipAccessKeyRegexp, + '[' + util.tooltipAccessKeyPrefix + '$5]' ); } + return val; } ); }, @@ -291,7 +285,7 @@ * A jQuery object that refers to the page-content element * Populated by init(). */ - '$content' : null, + $content: null, /** * Add a link to a portlet menu on the page, such as: @@ -328,14 +322,15 @@ * @return mixed The DOM Node of the added item (a ListItem or Anchor element, * depending on the skin) or null if no element was added to the document. */ - 'addPortletLink' : function( portlet, href, text, id, tooltip, accesskey, nextnode ) { + addPortletLink: function ( portlet, href, text, id, tooltip, accesskey, nextnode ) { + var $item, $link, $portlet, $ul; // Check if there's atleast 3 arguments to prevent a TypeError if ( arguments.length < 3 ) { return null; } // Setup the anchor tag - var $link = $( '<a></a>' ).attr( 'href', href ).text( text ); + $link = $( '<a>' ).attr( 'href', href ).text( text ); if ( tooltip ) { $link.attr( 'title', tooltip ); } @@ -343,22 +338,22 @@ // Some skins don't have any portlets // just add it to the bottom of their 'sidebar' element as a fallback switch ( mw.config.get( 'skin' ) ) { - case 'standard' : - case 'cologneblue' : - $( '#quickbar' ).append( $link.after( '<br />' ) ); + case 'standard': + case 'cologneblue': + $( '#quickbar' ).append( $link.after( '<br/>' ) ); return $link[0]; - case 'nostalgia' : - $( '#searchform' ).before( $link).before( ' | ' ); + case 'nostalgia': + $( '#searchform' ).before( $link ).before( ' | ' ); return $link[0]; - default : // Skins like chick, modern, monobook, myskin, simple, vector... + default: // Skins like chick, modern, monobook, myskin, simple, vector... // Select the specified portlet - var $portlet = $( '#' + portlet ); + $portlet = $( '#' + portlet ); if ( $portlet.length === 0 ) { return null; } // Select the first (most likely only) unordered list inside the portlet - var $ul = $portlet.find( 'ul' ); + $ul = $portlet.find( 'ul' ); // If it didn't have an unordered list yet, create it if ( $ul.length === 0 ) { @@ -383,7 +378,6 @@ // Wrap the anchor tag in a list item (and a span if $portlet is a Vector tab) // and back up the selector to the list item - var $item; if ( $portlet.hasClass( 'vectorTabs' ) ) { $item = $link.wrap( '<li><span></span></li>' ).parent().parent(); } else { @@ -400,19 +394,18 @@ $link.attr( 'title', tooltip ); } if ( accesskey && tooltip ) { - this.updateTooltipAccessKeys( $link ); + util.updateTooltipAccessKeys( $link ); } // Where to put our node ? - // - nextnode is a DOM element (before MW 1.17, in wikibits.js, this was the only option) - if ( nextnode && nextnode.parentNode == $ul[0] ) { + // - nextnode is a DOM element (was the only option before MW 1.17, in wikibits.js) + if ( nextnode && nextnode.parentNode === $ul[0] ) { $(nextnode).before( $item ); // - nextnode is a CSS selector for jQuery - } else if ( typeof nextnode == 'string' && $ul.find( nextnode ).length !== 0 ) { + } else if ( typeof nextnode === 'string' && $ul.find( nextnode ).length !== 0 ) { $ul.find( nextnode ).eq( 0 ).before( $item ); - // If the jQuery selector isn't found within the <ul>, // or if nextnode was invalid or not passed at all, // then just append it at the end of the <ul> (this is the default behaviour) @@ -430,15 +423,13 @@ * something, replacing any previous message. * Calling with no arguments, with an empty string or null will hide the message * - * @param message mixed The DOM-element or HTML-string to be put inside the message box. - * @param className string Used in adding a class; should be different for each call + * @param message {mixed} The DOM-element, jQuery object or HTML-string to be put inside the message box. + * @param className {String} Used in adding a class; should be different for each call * to allow CSS/JS to hide different boxes. null = no class used. - * @return boolean True on success, false on failure. + * @return {Boolean} True on success, false on failure. */ - 'jsMessage' : function( message, className ) { - + jsMessage: function ( message, className ) { if ( !arguments.length || message === '' || message === null ) { - $( '#mw-js-message' ).empty().hide(); return true; // Emptying and hiding message is intended behaviour, return true @@ -448,7 +439,7 @@ // an mw-js-message div to start with. var $messageDiv = $( '#mw-js-message' ); if ( !$messageDiv.length ) { - $messageDiv = $( '<div id="mw-js-message">' ); + $messageDiv = $( '<div id="mw-js-message"></div>' ); if ( util.$content.parent().length ) { util.$content.parent().prepend( $messageDiv ); } else { @@ -457,12 +448,12 @@ } if ( className ) { - $messageDiv.attr( 'class', 'mw-js-message-' + className ); + $messageDiv.prop( 'class', 'mw-js-message-' + className ); } if ( typeof message === 'object' ) { $messageDiv.empty(); - $messageDiv.append( message ); // Append new content + $messageDiv.append( message ); } else { $messageDiv.html( message ); } @@ -483,8 +474,10 @@ * @return mixed Null if mailtxt was an empty string, otherwise true/false * is determined by validation. */ - 'validateEmail' : function( mailtxt ) { - if( mailtxt === '' ) { + validateEmail: function ( mailtxt ) { + var rfc5322_atext, rfc1034_ldh_str, HTML5_email_regexp; + + if ( mailtxt === '' ) { return null; } @@ -500,7 +493,7 @@ */ /** - * First, define the RFC 5322 'atext' which is pretty easy : + * First, define the RFC 5322 'atext' which is pretty easy: * atext = ALPHA / DIGIT / ; Printable US-ASCII "!" / "#" / ; characters not including "$" / "%" / ; specials. Used for atoms. @@ -513,7 +506,7 @@ "|" / "}" / "~" */ - var rfc5322_atext = "a-z0-9!#$%&'*+\\-/=?^_`{|}~", + rfc5322_atext = "a-z0-9!#$%&'*+\\-/=?^_`{|}~"; /** * Next define the RFC 1034 'ldh-str' @@ -524,29 +517,29 @@ * <let-dig-hyp> ::= <let-dig> | "-" * <let-dig> ::= <letter> | <digit> */ - rfc1034_ldh_str = "a-z0-9\\-", - - HTML5_email_regexp = new RegExp( - // start of string - '^' - + - // User part which is liberal :p - '[' + rfc5322_atext + '\\.]+' - + - // 'at' - '@' - + - // Domain first part - '[' + rfc1034_ldh_str + ']+' - + - // Optional second part and following are separated by a dot - '(?:\\.[' + rfc1034_ldh_str + ']+)*' - + - // End of string - '$', - // RegExp is case insensitive - 'i' - ); + rfc1034_ldh_str = "a-z0-9\\-"; + + HTML5_email_regexp = new RegExp( + // start of string + '^' + + + // User part which is liberal :p + '[' + rfc5322_atext + '\\.]+' + + + // 'at' + '@' + + + // Domain first part + '[' + rfc1034_ldh_str + ']+' + + + // Optional second part and following are separated by a dot + '(?:\\.[' + rfc1034_ldh_str + ']+)*' + + + // End of string + '$', + // RegExp is case insensitive + 'i' + ); return (null !== mailtxt.match( HTML5_email_regexp ) ); }, @@ -557,12 +550,18 @@ * @param allowBlock boolean * @return boolean */ - 'isIPv4Address' : function( address, allowBlock ) { - var block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : ''; - var RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])'; - var RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE; - return typeof address === 'string' && address.search( new RegExp( '^' + RE_IP_ADD + block + '$' ) ) != -1; + isIPv4Address: function ( address, allowBlock ) { + if ( typeof address !== 'string' ) { + return false; + } + + var block = allowBlock ? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '', + RE_IP_BYTE = '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])', + RE_IP_ADD = '(?:' + RE_IP_BYTE + '\\.){3}' + RE_IP_BYTE; + + return address.search( new RegExp( '^' + RE_IP_ADD + block + '$' ) ) !== -1; }, + /** * Note: borrows from IP::isIPv6 * @@ -570,12 +569,13 @@ * @param allowBlock boolean * @return boolean */ - 'isIPv6Address' : function( address, allowBlock ) { + isIPv6Address: function ( address, allowBlock ) { if ( typeof address !== 'string' ) { return false; } - var block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : ''; - var RE_IPV6_ADD = + + var block = allowBlock ? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '', + RE_IPV6_ADD = '(?:' + // starts with "::" (including "::") ':(?::|(?::' + '[0-9A-Fa-f]{1,4}' + '){1,7})' + '|' + // ends with "::" (except "::") @@ -583,17 +583,19 @@ '|' + // contains no "::" '[0-9A-Fa-f]{1,4}' + '(?::' + '[0-9A-Fa-f]{1,4}' + '){7}' + ')'; - if ( address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) != -1 ) { + + if ( address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1 ) { return true; } + RE_IPV6_ADD = // contains one "::" in the middle (single '::' check below) '[0-9A-Fa-f]{1,4}' + '(?:::?' + '[0-9A-Fa-f]{1,4}' + '){1,6}'; - return address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) != -1 - && address.search( /::/ ) != -1 && address.search( /::.*::/ ) == -1; - } + return address.search( new RegExp( '^' + RE_IPV6_ADD + block + '$' ) ) !== -1 + && address.search( /::/ ) !== -1 && address.search( /::.*::/ ) === -1; + } }; - util.init(); + mw.util = util; -} )( jQuery ); +} )( jQuery, mediaWiki ); |