Index: trunk/phase3/resources/mediawiki/mediawiki.Uri.js |
— | — | @@ -56,7 +56,7 @@ |
57 | 57 | * |
58 | 58 | */ |
59 | 59 | |
60 | | -( function( $ ) { |
| 60 | +( function( $, mw ) { |
61 | 61 | |
62 | 62 | /** |
63 | 63 | * Function that's useful when constructing the URI string -- we frequently encounter the pattern of |
— | — | @@ -93,168 +93,191 @@ |
94 | 94 | 'fragment' // top |
95 | 95 | ]; |
96 | 96 | |
97 | | - /** |
98 | | - * Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse. |
99 | | - * @constructor |
100 | | - * @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. |
101 | | - * @param {Boolean} strict mode (when parsing a string) |
102 | | - */ |
103 | | - mw.Uri = function( uri, strictMode ) { |
104 | | - strictMode = !!strictMode; |
105 | | - if ( uri !== undefined && uri !== null || uri !== '' ) { |
106 | | - if ( typeof uri === 'string' ) { |
107 | | - this._parse( uri, strictMode ); |
108 | | - } else if ( typeof uri === 'object' ) { |
109 | | - var _this = this; |
110 | | - $.each( properties, function( i, property ) { |
111 | | - _this[property] = uri[property]; |
112 | | - } ); |
113 | | - if ( this.query === undefined ) { |
114 | | - this.query = {}; |
115 | | - } |
116 | | - } |
117 | | - } |
118 | | - if ( !( this.protocol && this.host && this.path ) ) { |
119 | | - throw new Error( 'Bad constructor arguments' ); |
120 | | - } |
121 | | - }; |
122 | 97 | |
123 | 98 | /** |
124 | | - * Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more compliant with RFC 3986 |
125 | | - * Similar to rawurlencode from PHP and our JS library mw.util.rawurlencode, but we also replace space with a + |
126 | | - * @param {String} string |
127 | | - * @return {String} encoded for URI |
| 99 | + * We use a factory to inject a document location, for relative URLs, including protocol-relative URLs. |
| 100 | + * so the library is still testable & purely functional. |
128 | 101 | */ |
129 | | - mw.Uri.encode = function( s ) { |
130 | | - return encodeURIComponent( s ) |
131 | | - .replace( /!/g, '%21').replace( /'/g, '%27').replace( /\(/g, '%28') |
132 | | - .replace( /\)/g, '%29').replace( /\*/g, '%2A') |
133 | | - .replace( /%20/g, '+' ); |
134 | | - }; |
| 102 | + mw.UriRelative = function( documentLocation ) { |
135 | 103 | |
136 | | - /** |
137 | | - * Standard decodeURIComponent, with '+' to space |
138 | | - * @param {String} string encoded for URI |
139 | | - * @return {String} decoded string |
140 | | - */ |
141 | | - mw.Uri.decode = function( s ) { |
142 | | - return decodeURIComponent( s ).replace( /\+/g, ' ' ); |
143 | | - }; |
144 | | - |
145 | | - mw.Uri.prototype = { |
146 | | - |
147 | 104 | /** |
148 | | - * Parse a string and set our properties accordingly. |
149 | | - * @param {String} URI |
150 | | - * @param {Boolean} strictness |
151 | | - * @return {Boolean} success |
| 105 | + * Constructs URI object. Throws error if arguments are illegal/impossible, or otherwise don't parse. |
| 106 | + * @constructor |
| 107 | + * @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. |
| 108 | + * @param {Boolean} strict mode (when parsing a string) |
152 | 109 | */ |
153 | | - _parse: function( str, strictMode ) { |
154 | | - var matches = parser[ strictMode ? 'strict' : 'loose' ].exec( str ); |
155 | | - var uri = this; |
156 | | - $.each( properties, function( i, property ) { |
157 | | - uri[ property ] = matches[ i+1 ]; |
158 | | - } ); |
159 | | - |
160 | | - // uri.query starts out as the query string; we will parse it into key-val pairs then make |
161 | | - // that object the "query" property. |
162 | | - // we overwrite query in uri way to make cloning easier, it can use the same list of properties. |
163 | | - var q = {}; |
164 | | - // using replace to iterate over a string |
165 | | - if ( uri.query ) { |
166 | | - uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { |
167 | | - if ( $1 ) { |
168 | | - var k = mw.Uri.decode( $1 ); |
169 | | - var v = ( $2 === '' || $2 === undefined ) ? null : mw.Uri.decode( $3 ); |
170 | | - if ( typeof q[ k ] === 'string' ) { |
171 | | - q[ k ] = [ q[ k ] ]; |
172 | | - } |
173 | | - if ( typeof q[ k ] === 'object' ) { |
174 | | - q[ k ].push( v ); |
175 | | - } else { |
176 | | - q[ k ] = v; |
177 | | - } |
| 110 | + function Uri( uri, strictMode ) { |
| 111 | + strictMode = !!strictMode; |
| 112 | + if ( uri !== undefined && uri !== null || uri !== '' ) { |
| 113 | + if ( typeof uri === 'string' ) { |
| 114 | + this._parse( uri, strictMode ); |
| 115 | + } else if ( typeof uri === 'object' ) { |
| 116 | + var _this = this; |
| 117 | + $.each( properties, function( i, property ) { |
| 118 | + _this[property] = uri[property]; |
| 119 | + } ); |
| 120 | + if ( this.query === undefined ) { |
| 121 | + this.query = {}; |
178 | 122 | } |
179 | | - } ); |
| 123 | + } |
180 | 124 | } |
181 | | - this.query = q; |
182 | | - }, |
183 | 125 | |
184 | | - /** |
185 | | - * Returns user and password portion of a URI. |
186 | | - * @return {String} |
187 | | - */ |
188 | | - getUserInfo: function() { |
189 | | - return cat( '', this.user, cat( ':', this.password, '' ) ); |
190 | | - }, |
| 126 | + // protocol-relative URLs |
| 127 | + if ( !this.protocol ) { |
| 128 | + this.protocol = defaultProtocol; |
| 129 | + } |
191 | 130 | |
| 131 | + if ( !( this.protocol && this.host && this.path ) ) { |
| 132 | + throw new Error( 'Bad constructor arguments' ); |
| 133 | + } |
| 134 | + } |
| 135 | + |
192 | 136 | /** |
193 | | - * Gets host and port portion of a URI. |
194 | | - * @return {String} |
| 137 | + * Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more compliant with RFC 3986 |
| 138 | + * Similar to rawurlencode from PHP and our JS library mw.util.rawurlencode, but we also replace space with a + |
| 139 | + * @param {String} string |
| 140 | + * @return {String} encoded for URI |
195 | 141 | */ |
196 | | - getHostPort: function() { |
197 | | - return this.host + cat( ':', this.port, '' ); |
198 | | - }, |
| 142 | + Uri.encode = function( s ) { |
| 143 | + return encodeURIComponent( s ) |
| 144 | + .replace( /!/g, '%21').replace( /'/g, '%27').replace( /\(/g, '%28') |
| 145 | + .replace( /\)/g, '%29').replace( /\*/g, '%2A') |
| 146 | + .replace( /%20/g, '+' ); |
| 147 | + }; |
199 | 148 | |
200 | 149 | /** |
201 | | - * Returns the userInfo and host and port portion of the URI. |
202 | | - * In most real-world URLs, this is simply the hostname, but it is more general. |
203 | | - * @return {String} |
| 150 | + * Standard decodeURIComponent, with '+' to space |
| 151 | + * @param {String} string encoded for URI |
| 152 | + * @return {String} decoded string |
204 | 153 | */ |
205 | | - getAuthority: function() { |
206 | | - return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); |
207 | | - }, |
| 154 | + Uri.decode = function( s ) { |
| 155 | + return decodeURIComponent( s ).replace( /\+/g, ' ' ); |
| 156 | + }; |
208 | 157 | |
209 | | - /** |
210 | | - * Returns the query arguments of the URL, encoded into a string |
211 | | - * Does not preserve the order of arguments passed into the URI. Does handle escaping. |
212 | | - * @return {String} |
213 | | - */ |
214 | | - getQueryString: function() { |
215 | | - var args = []; |
216 | | - $.each( this.query, function( key, val ) { |
217 | | - var k = mw.Uri.encode( key ); |
218 | | - var vals = val === null ? [ null ] : $.makeArray( val ); |
219 | | - $.each( vals, function( i, v ) { |
220 | | - args.push( k + ( v === null ? '' : '=' + mw.Uri.encode( v ) ) ); |
| 158 | + Uri.prototype = { |
| 159 | + |
| 160 | + /** |
| 161 | + * Parse a string and set our properties accordingly. |
| 162 | + * @param {String} URI |
| 163 | + * @param {Boolean} strictness |
| 164 | + * @return {Boolean} success |
| 165 | + */ |
| 166 | + _parse: function( str, strictMode ) { |
| 167 | + var matches = parser[ strictMode ? 'strict' : 'loose' ].exec( str ); |
| 168 | + var uri = this; |
| 169 | + $.each( properties, function( i, property ) { |
| 170 | + uri[ property ] = matches[ i+1 ]; |
221 | 171 | } ); |
222 | | - } ); |
223 | | - return args.join( '&' ); |
224 | | - }, |
225 | 172 | |
226 | | - /** |
227 | | - * Returns everything after the authority section of the URI |
228 | | - * @return {String} |
229 | | - */ |
230 | | - getRelativePath: function() { |
231 | | - return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); |
232 | | - }, |
| 173 | + // uri.query starts out as the query string; we will parse it into key-val pairs then make |
| 174 | + // that object the "query" property. |
| 175 | + // we overwrite query in uri way to make cloning easier, it can use the same list of properties. |
| 176 | + var q = {}; |
| 177 | + // using replace to iterate over a string |
| 178 | + if ( uri.query ) { |
| 179 | + uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ($0, $1, $2, $3) { |
| 180 | + if ( $1 ) { |
| 181 | + var k = Uri.decode( $1 ); |
| 182 | + var v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 ); |
| 183 | + if ( typeof q[ k ] === 'string' ) { |
| 184 | + q[ k ] = [ q[ k ] ]; |
| 185 | + } |
| 186 | + if ( typeof q[ k ] === 'object' ) { |
| 187 | + q[ k ].push( v ); |
| 188 | + } else { |
| 189 | + q[ k ] = v; |
| 190 | + } |
| 191 | + } |
| 192 | + } ); |
| 193 | + } |
| 194 | + this.query = q; |
| 195 | + }, |
233 | 196 | |
234 | | - /** |
235 | | - * Gets the entire URI string. May not be precisely the same as input due to order of query arguments. |
236 | | - * @return {String} the URI string |
237 | | - */ |
238 | | - toString: function() { |
239 | | - return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); |
240 | | - }, |
| 197 | + /** |
| 198 | + * Returns user and password portion of a URI. |
| 199 | + * @return {String} |
| 200 | + */ |
| 201 | + getUserInfo: function() { |
| 202 | + return cat( '', this.user, cat( ':', this.password, '' ) ); |
| 203 | + }, |
241 | 204 | |
242 | | - /** |
243 | | - * Clone this URI |
244 | | - * @return {Object} new URI object with same properties |
245 | | - */ |
246 | | - clone: function() { |
247 | | - return new mw.Uri( this ); |
248 | | - }, |
| 205 | + /** |
| 206 | + * Gets host and port portion of a URI. |
| 207 | + * @return {String} |
| 208 | + */ |
| 209 | + getHostPort: function() { |
| 210 | + return this.host + cat( ':', this.port, '' ); |
| 211 | + }, |
249 | 212 | |
250 | | - /** |
251 | | - * Extend the query -- supply query parameters to override or add to ours |
252 | | - * @param {Object} query parameters in key-val form to override or add |
253 | | - * @return {Object} this URI object |
254 | | - */ |
255 | | - extend: function( parameters ) { |
256 | | - $.extend( this.query, parameters ); |
257 | | - return this; |
258 | | - } |
| 213 | + /** |
| 214 | + * Returns the userInfo and host and port portion of the URI. |
| 215 | + * In most real-world URLs, this is simply the hostname, but it is more general. |
| 216 | + * @return {String} |
| 217 | + */ |
| 218 | + getAuthority: function() { |
| 219 | + return cat( '', this.getUserInfo(), '@' ) + this.getHostPort(); |
| 220 | + }, |
| 221 | + |
| 222 | + /** |
| 223 | + * Returns the query arguments of the URL, encoded into a string |
| 224 | + * Does not preserve the order of arguments passed into the URI. Does handle escaping. |
| 225 | + * @return {String} |
| 226 | + */ |
| 227 | + getQueryString: function() { |
| 228 | + var args = []; |
| 229 | + $.each( this.query, function( key, val ) { |
| 230 | + var k = Uri.encode( key ); |
| 231 | + var vals = val === null ? [ null ] : $.makeArray( val ); |
| 232 | + $.each( vals, function( i, v ) { |
| 233 | + args.push( k + ( v === null ? '' : '=' + Uri.encode( v ) ) ); |
| 234 | + } ); |
| 235 | + } ); |
| 236 | + return args.join( '&' ); |
| 237 | + }, |
| 238 | + |
| 239 | + /** |
| 240 | + * Returns everything after the authority section of the URI |
| 241 | + * @return {String} |
| 242 | + */ |
| 243 | + getRelativePath: function() { |
| 244 | + return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' ); |
| 245 | + }, |
| 246 | + |
| 247 | + /** |
| 248 | + * Gets the entire URI string. May not be precisely the same as input due to order of query arguments. |
| 249 | + * @return {String} the URI string |
| 250 | + */ |
| 251 | + toString: function() { |
| 252 | + return this.protocol + '://' + this.getAuthority() + this.getRelativePath(); |
| 253 | + }, |
| 254 | + |
| 255 | + /** |
| 256 | + * Clone this URI |
| 257 | + * @return {Object} new URI object with same properties |
| 258 | + */ |
| 259 | + clone: function() { |
| 260 | + return new Uri( this ); |
| 261 | + }, |
| 262 | + |
| 263 | + /** |
| 264 | + * Extend the query -- supply query parameters to override or add to ours |
| 265 | + * @param {Object} query parameters in key-val form to override or add |
| 266 | + * @return {Object} this URI object |
| 267 | + */ |
| 268 | + extend: function( parameters ) { |
| 269 | + $.extend( this.query, parameters ); |
| 270 | + return this; |
| 271 | + } |
| 272 | + }; |
| 273 | + |
| 274 | + var defaultProtocol = ( new Uri( documentLocation ) ).protocol; |
| 275 | + |
| 276 | + return Uri; |
259 | 277 | }; |
260 | 278 | |
261 | | -} )( jQuery ); |
| 279 | + // inject the current document location, for relative URLs |
| 280 | + mw.Uri = mw.UriRelative( document.location.href ); |
| 281 | + |
| 282 | + |
| 283 | + |
| 284 | +} )( jQuery, mediaWiki ); |
Index: trunk/phase3/tests/jasmine/spec/mediawiki.Uri.spec.js |
— | — | @@ -7,7 +7,7 @@ |
8 | 8 | function basicTests( strict ) { |
9 | 9 | |
10 | 10 | describe( "should parse a simple HTTP URI correctly", function() { |
11 | | - |
| 11 | + |
12 | 12 | var uriString = 'http://www.ietf.org/rfc/rfc2396.txt'; |
13 | 13 | var uri; |
14 | 14 | if ( strict ) { |
— | — | @@ -238,6 +238,16 @@ |
239 | 239 | |
240 | 240 | } ); |
241 | 241 | |
| 242 | + describe( "should handle protocol-relative URLs", function() { |
| 243 | + |
| 244 | + it ( "should create protocol-relative URLs with same protocol as document", function() { |
| 245 | + var uriRel = mw.UriRelative( 'glork://en.wiki.local/foo.php' ); |
| 246 | + var uri = new uriRel( '//en.wiki.local/w/api.php' ); |
| 247 | + expect( uri.protocol ).toEqual( 'glork' ); |
| 248 | + } ); |
| 249 | + |
| 250 | + } ); |
| 251 | + |
242 | 252 | it( "should throw error on no arguments to constructor", function() { |
243 | 253 | expect( function() { |
244 | 254 | var uri = new mw.Uri(); |
— | — | @@ -262,12 +272,17 @@ |
263 | 273 | } ).toThrow( "Bad constructor arguments" ); |
264 | 274 | } ); |
265 | 275 | |
266 | | - it( "should throw error on URI without protocol as argument to constructor", function() { |
| 276 | + it( "should throw error on URI without protocol or // in strict mode", function() { |
267 | 277 | expect( function() { |
268 | | - var uri = new mw.Uri( 'foo.com/bar/baz' ); |
| 278 | + var uri = new mw.Uri( 'foo.com/bar/baz', true ); |
269 | 279 | } ).toThrow( "Bad constructor arguments" ); |
270 | 280 | } ); |
271 | 281 | |
| 282 | + it( "should normalize URI without protocol or // in loose mode", function() { |
| 283 | + var uri = new mw.Uri( 'foo.com/bar/baz', false ); |
| 284 | + expect( uri.toString() ).toEqual( 'http://foo.com/bar/baz' ); |
| 285 | + } ); |
| 286 | + |
272 | 287 | } ); |
273 | 288 | |
274 | 289 | } )(); |