* @param {boolean} [options.strictMode=false] Trigger strict mode parsing of the url.
* @param {boolean} [options.overrideKeys=false] Whether to let duplicate query parameters
* override each other (`true`) or automagically convert them to an array (`false`).
+ * @param {boolean} [options.arrayParams=false] Whether to parse array query parameters (e.g.
+ * `&foo[0]=a&foo[1]=b` or `&foo[]=a&foo[]=b`) or leave them alone. Currently this does not
+ * handle associative or multi-dimensional arrays, but that may be improved in the future.
+ * Implies `overrideKeys: true` (query parameters without `[...]` are not parsed as arrays).
* @throws {Error} when the query string or fragment contains an unknown % sequence
*/
function Uri( uri, options ) {
options = typeof options === 'object' ? options : { strictMode: !!options };
options = $.extend( {
strictMode: false,
- overrideKeys: false
+ overrideKeys: false,
+ arrayParams: false
}, options );
+ this.arrayParams = options.arrayParams;
+
if ( uri !== undefined && uri !== null && uri !== '' ) {
if ( typeof uri === 'string' ) {
this.parse( uri, options );
// using replace to iterate over a string
if ( uri.query ) {
uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( match, k, eq, v ) {
+ var arrayKeyMatch, i;
if ( k ) {
k = Uri.decode( k );
v = ( eq === '' || eq === undefined ) ? null : Uri.decode( v );
+ arrayKeyMatch = k.match( /^([^[]+)\[(\d*)\]$/ );
+
+ // If arrayParams and this parameter name contains an array index...
+ if ( options.arrayParams && arrayKeyMatch ) {
+ // Remove the index from parameter name
+ k = arrayKeyMatch[ 1 ];
+
+ // Turn the parameter value into an array (throw away anything else)
+ if ( !Array.isArray( q[ k ] ) ) {
+ q[ k ] = [];
+ }
+
+ i = arrayKeyMatch[ 2 ];
+ if ( i === '' ) {
+ // If no explicit index, append at the end
+ i = q[ k ].length;
+ }
+
+ q[ k ][ i ] = v;
// 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 || !hasOwn.call( q, k ) ) {
+ // arrayParams implies overrideKeys (no array handling for non-array params).
+ } else if ( options.arrayParams || options.overrideKeys || !hasOwn.call( q, k ) ) {
q[ k ] = v;
// Use arrays if overrideKeys is false and key was already seen before
* @return {string}
*/
getQueryString: function () {
- var args = [];
+ var args = [],
+ arrayParams = this.arrayParams;
// eslint-disable-next-line no-jquery/no-each-util
$.each( this.query, function ( key, val ) {
var k = Uri.encode( key ),
- vals = Array.isArray( val ) ? val : [ val ];
- vals.forEach( function ( v ) {
+ isArrayParam = Array.isArray( val ),
+ vals = isArrayParam ? val : [ val ];
+ vals.forEach( function ( v, i ) {
+ var ki = k;
+ if ( arrayParams && isArrayParam ) {
+ ki += Uri.encode( '[' + i + ']' );
+ }
if ( v === null ) {
- args.push( k );
+ args.push( ki );
} else if ( k === 'title' ) {
- args.push( k + '=' + mw.util.wikiUrlencode( v ) );
+ args.push( ki + '=' + mw.util.wikiUrlencode( v ) );
} else {
- args.push( k + '=' + Uri.encode( v ) );
+ args.push( ki + '=' + Uri.encode( v ) );
}
} );
} );
} );
+ QUnit.test( 'arrayParams', function ( assert ) {
+ var uri1, uri2, uri3, expectedQ, expectedS,
+ uriMissing, expectedMissingQ, expectedMissingS,
+ uriWeird, expectedWeirdQ, expectedWeirdS;
+
+ uri1 = new mw.Uri( 'http://example.com/?foo[]=a&foo[]=b&foo[]=c', { arrayParams: true } );
+ uri2 = new mw.Uri( 'http://example.com/?foo[0]=a&foo[1]=b&foo[2]=c', { arrayParams: true } );
+ uri3 = new mw.Uri( 'http://example.com/?foo[1]=b&foo[0]=a&foo[]=c', { arrayParams: true } );
+ expectedQ = { foo: [ 'a', 'b', 'c' ] };
+ expectedS = 'foo%5B0%5D=a&foo%5B1%5D=b&foo%5B2%5D=c';
+
+ assert.deepEqual( uri1.query, expectedQ,
+ 'array query parameters are parsed (implicit indexes)' );
+ assert.deepEqual( uri1.getQueryString(), expectedS,
+ 'array query parameters are encoded (always with explicit indexes)' );
+ assert.deepEqual( uri2.query, expectedQ,
+ 'array query parameters are parsed (explicit indexes)' );
+ assert.deepEqual( uri2.getQueryString(), expectedS,
+ 'array query parameters are encoded (always with explicit indexes)' );
+ assert.deepEqual( uri3.query, expectedQ,
+ 'array query parameters are parsed (mixed indexes, out of order)' );
+ assert.deepEqual( uri3.getQueryString(), expectedS,
+ 'array query parameters are encoded (always with explicit indexes)' );
+
+ uriMissing = new mw.Uri( 'http://example.com/?foo[0]=a&foo[2]=c', { arrayParams: true } );
+ // eslint-disable-next-line no-sparse-arrays
+ expectedMissingQ = { foo: [ 'a', , 'c' ] };
+ expectedMissingS = 'foo%5B0%5D=a&foo%5B2%5D=c';
+
+ assert.deepEqual( uriMissing.query, expectedMissingQ,
+ 'array query parameters are parsed (missing array item)' );
+ assert.deepEqual( uriMissing.getQueryString(), expectedMissingS,
+ 'array query parameters are encoded (missing array item)' );
+
+ uriWeird = new mw.Uri( 'http://example.com/?foo[0]=a&foo[1][1]=b&foo[x]=c', { arrayParams: true } );
+ expectedWeirdQ = { foo: [ 'a' ], 'foo[1][1]': 'b', 'foo[x]': 'c' };
+ expectedWeirdS = 'foo%5B0%5D=a&foo%5B1%5D%5B1%5D=b&foo%5Bx%5D=c';
+
+ assert.deepEqual( uriWeird.query, expectedWeirdQ,
+ 'array query parameters are parsed (multi-dimensional or associative arrays are ignored)' );
+ assert.deepEqual( uriWeird.getQueryString(), expectedWeirdS,
+ 'array query parameters are encoded (multi-dimensional or associative arrays are ignored)' );
+ } );
+
QUnit.test( '.clone()', function ( assert ) {
var original, clone;