fetchLanguageNames: fallback to default instead of false
[lhc/web/wiklou.git] / resources / mediawiki / mediawiki.Uri.js
index 7ff8dda..2957674 100644 (file)
@@ -56,7 +56,7 @@
  *
  */
 
-( function( $ ) {
+( function( $, mw ) {
 
        /**
         * Function that's useful when constructing the URI string -- we frequently encounter the pattern of
                '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 = defaultUri.protocol;
+                       }
+                       // No host given:
+                       if ( !this.host ) {
+                               this.host = defaultUri.host;
+                               // port ?
+                               if ( !this.port ) {
+                                       this.port = defaultUri.port;
+                               }
+                       }
+                       if ( this.path && this.path.charAt( 0 ) !== '/' ) {
+                               // A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot
+                               // figure out whether the last path compoennt of defaultUri.path is a directory or a file.
+                               throw new Error( 'Bad constructor arguments' );
+                       }
+                       if ( !( this.protocol && this.host && this.path ) ) {
+                               throw new Error( 'Bad constructor arguments' );
+                       }
+               }
 
                /**
-                * Gets host and port portion of a URI.
-                * @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
                 */
-               getHostPort: function() {
-                       return this.host + cat( ':', this.port, '' );
-               },
+               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 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 decodeURIComponent, with '+' to space
+                * @param {String} string encoded for URI
+                * @return {String} decoded string
                 */
-               getAuthority: function() {
-                       return cat( '', this.getUserInfo(), '@' ) + this.getHostPort();
-               },
+               Uri.decode = function( s ) {
+                       return decodeURIComponent( s.replace( /\+/g, '%20' ) );
+               };
 
-               /**
-                * 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 = 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.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 defaultUri = new Uri( documentLocation );
+
+               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 );