From 2300a5173bae340ea761db63fbe45afaa776870a Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 28 Jan 2017 10:37:45 -0800 Subject: [PATCH 1/8] url: extend URLSearchParams constructor Fixes: https://github.com/nodejs/node/issues/10635 Refs: https://github.com/whatwg/url/pull/175 Refs: https://github.com/w3c/web-platform-tests/pull/4523 --- lib/internal/url.js | 45 ++++++++++- ...est-whatwg-url-searchparams-constructor.js | 78 ++++++++++++++++--- 2 files changed, 110 insertions(+), 13 deletions(-) diff --git a/lib/internal/url.js b/lib/internal/url.js index 84e04ee9d8fa85..0c71c57acec280 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -649,10 +649,49 @@ function getObjectFromParams(array) { class URLSearchParams { constructor(init = '') { - if (init instanceof URLSearchParams) { - const childParams = init[searchParams]; - this[searchParams] = childParams.slice(); + if (init === null || init === undefined) { + // record + this[searchParams] = []; + } else if (typeof init === 'object') { + const method = init[Symbol.iterator]; + if (method === this[Symbol.iterator]) { + // While the spec does not have this branch, we can use it as a + // shortcut to avoid having to go through the costly generic iterator. + const childParams = init[searchParams]; + this[searchParams] = childParams.slice(); + } else if (method !== null && method !== undefined) { + if (typeof method !== 'function') { + throw new TypeError('Query pairs must be iterable'); + } + + // sequence> + // Note: per spec we have to first exhaust the lists then process them + const pairs = []; + for (const pair of init) { + if (typeof pair !== 'object' || + typeof pair[Symbol.iterator] !== 'function') { + throw new TypeError('Each query pair must be iterable'); + } + pairs.push(Array.from(pair)); + } + + this[searchParams] = []; + for (const pair of pairs) { + if (pair.length !== 2) { + throw new TypeError('Each query pair must be a name/value tuple'); + } + this[searchParams].push(String(pair[0]), String(pair[1])); + } + } else { + // record + this[searchParams] = []; + for (const key of Object.keys(init)) { + const value = String(init[key]); + this[searchParams].push(key, value); + } + } } else { + // USVString init = String(init); if (init[0] === '?') init = init.slice(1); initSearchParams(this, init); diff --git a/test/parallel/test-whatwg-url-searchparams-constructor.js b/test/parallel/test-whatwg-url-searchparams-constructor.js index 9b7ca1e4e9c54a..be2379a0390f31 100644 --- a/test/parallel/test-whatwg-url-searchparams-constructor.js +++ b/test/parallel/test-whatwg-url-searchparams-constructor.js @@ -16,23 +16,24 @@ assert.strictEqual(params + '', 'a=b'); params = new URLSearchParams(params); assert.strictEqual(params + '', 'a=b'); -// URLSearchParams constructor, empty. +// URLSearchParams constructor, no arguments +params = new URLSearchParams(); +assert.strictEqual(params.toString(), ''); + assert.throws(() => URLSearchParams(), TypeError, 'Calling \'URLSearchParams\' without \'new\' should throw.'); -// assert.throws(() => new URLSearchParams(DOMException.prototype), TypeError); -assert.throws(() => { - new URLSearchParams({ - toString() { throw new TypeError('Illegal invocation'); } - }); -}, TypeError); + +// URLSearchParams constructor, empty string as argument params = new URLSearchParams(''); -assert.notStrictEqual(params, null, 'constructor returned non-null value.'); +// eslint-disable-next-line no-restricted-properties +assert.notEqual(params, null, 'constructor returned non-null value.'); // eslint-disable-next-line no-proto assert.strictEqual(params.__proto__, URLSearchParams.prototype, 'expected URLSearchParams.prototype as prototype.'); + +// URLSearchParams constructor, {} as argument params = new URLSearchParams({}); -// assert.strictEqual(params + '', '%5Bobject+Object%5D='); -assert.strictEqual(params + '', '%5Bobject%20Object%5D='); +assert.strictEqual(params + '', ''); // URLSearchParams constructor, string. params = new URLSearchParams('a=b'); @@ -128,3 +129,60 @@ params = new URLSearchParams('a=b%f0%9f%92%a9c'); assert.strictEqual(params.get('a'), 'b\uD83D\uDCA9c'); params = new URLSearchParams('a%f0%9f%92%a9b=c'); assert.strictEqual(params.get('a\uD83D\uDCA9b'), 'c'); + +// Constructor with sequence of sequences of strings +params = new URLSearchParams([]); +// eslint-disable-next-line no-restricted-properties +assert.notEqual(params, null, 'constructor returned non-null value.'); +params = new URLSearchParams([['a', 'b'], ['c', 'd']]); +assert.strictEqual(params.get('a'), 'b'); +assert.strictEqual(params.get('c'), 'd'); +assert.throws(() => new URLSearchParams([[1]]), + /^TypeError: Each query pair must be a name\/value tuple$/); +assert.throws(() => new URLSearchParams([[1, 2, 3]]), + /^TypeError: Each query pair must be a name\/value tuple$/); + +[ + // Further confirmation needed + // https://github.com/w3c/web-platform-tests/pull/4523#discussion_r98337513 + // { + // input: {'+': '%C2'}, + // output: [[' ', '\uFFFD']], + // name: 'object with +' + // }, + { + input: {c: 'x', a: '?'}, + output: [['c', 'x'], ['a', '?']], + name: 'object with two keys' + }, + { + input: [['c', 'x'], ['a', '?']], + output: [['c', 'x'], ['a', '?']], + name: 'array with two keys' + } +].forEach((val) => { + const params = new URLSearchParams(val.input); + let i = 0; + for (const param of params) { + assert.deepStrictEqual(param, val.output[i], + `Construct with ${val.name}`); + i++; + } +}); + +// Custom [Symbol.iterator] +params = new URLSearchParams(); +params[Symbol.iterator] = function *() { + yield ['a', 'b']; +}; +const params2 = new URLSearchParams(params); +assert.strictEqual(params2.get('a'), 'b'); + +assert.throws(() => new URLSearchParams({ [Symbol.iterator]: 42 }), + /^TypeError: Query pairs must be iterable$/); +assert.throws(() => new URLSearchParams([{}]), + /^TypeError: Each query pair must be iterable$/); +assert.throws(() => new URLSearchParams(['a']), + /^TypeError: Each query pair must be iterable$/); +assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]), + /^TypeError: Each query pair must be iterable$/); From e8a3c95629671bd1c9c8df670240825b8f1d7d9e Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 28 Jan 2017 12:02:20 -0800 Subject: [PATCH 2/8] tools: add MDN link for Iterable --- tools/doc/type-parser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 0ac15a14b93341..40dd2fa137f11e 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -36,6 +36,8 @@ const typeMap = { 'http.IncomingMessage': 'http.html#http_class_http_incomingmessage', 'http.Server': 'http.html#http_class_http_server', 'http.ServerResponse': 'http.html#http_class_http_serverresponse', + 'Iterable': jsDocPrefix + + 'Reference/Iteration_protocols#The_iterable_protocol', 'Iterator': jsDocPrefix + 'Reference/Iteration_protocols#The_iterator_protocol' }; From f1cc4f350de16ab5ce24b7f9ac80ba7886599f95 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 28 Jan 2017 12:02:35 -0800 Subject: [PATCH 3/8] doc: document URLSearchParams constructor --- doc/api/url.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/doc/api/url.md b/doc/api/url.md index 50e7433e6110e5..54fda577b5d0ff 100755 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -525,7 +525,8 @@ value returned is equivalent to that of `url.href`. ### Class: URLSearchParams The `URLSearchParams` object provides read and write access to the query of a -`URL`. +`URL`. It can also be used standalone with one of the four following +constructors. ```js const URL = require('url').URL; @@ -543,9 +544,121 @@ console.log(myURL.href); // Prints https://example.org/?a=b ``` -#### Constructor: new URLSearchParams([init]) +#### Constructor: new URLSearchParams() -* `init` {String} The URL query +Instantiate a new empty `URLSearchParams` object. + +#### Constructor: new URLSearchParams(string) + +* `string` {String} A query string + +Parse the `string` as a query string, and use it to instantiate a new +`URLSearchParams` object. A leading `'?'`, if present, is ignored. + +```js +const { URLSearchParams } = require('url'); +let params; + +params = new URLSearchParams('user=abc&query=xyz'); +console.log(params.get('user')); + // Prints 'abc' +console.log(params.toString()); + // Prints 'user=abc&query=xyz' + +params = new URLSearchParams('?user=abc&query=xyz'); +console.log(params.toString()); + // Prints 'user=abc&query=xyz' +``` + +#### Constructor: new URLSearchParams(obj) + +* `obj` {Object} An object representing a collection of key-value pairs + +Instantiate a new `URLSearchParams` object with a query hash map. The key and +value of each property of `obj` are always coerced to strings. + +Warning: Unlike [`querystring`][] module, duplicate keys in the form of array +values are not allowed. Arrays are stringified using [`array.toString()`][], +which simply joins all array elements with commas. + +```js +const { URLSearchParams } = require('url'); +const params = new URLSearchParams({ + user: 'abc', + query: ['first', 'second'] +}); +console.log(params.getAll('query')); + // Prints ['first,second'] +console.log(params.toString()); + // Prints 'user=abc&query=first%2Csecond' +``` + +#### Constructor: new URLSearchParams(iterable) + +* `iterable` {Iterable} An iterable object whose elements are key-value pairs + +Instantiate a new `URLSearchParams` object with an iterable map in a way that +is similar to [`Map`][]'s constructor. `iterable` can be an Array or any +iterable object. Elements of `iterable` are key-value pairs, and can themselves +be any iterable object. + +Duplicate keys are allowed. + +```js +const { URLSearchParams } = require('url'); +let params; + +// Using an array +params = new URLSearchParams([ + ['user', 'abc'], + ['query', 'first'], + ['query', 'second'] +]); +console.log(params.toString()); + // Prints 'user=abc&query=first&query=second' + +// Using a Map object +const map = new Map(); +map.set('user', 'abc'); +map.set('query', 'xyz'); +params = new URLSearchParams(map); +console.log(params.toString()); + // Prints 'user=abc&query=xyz' + +// Using a generator function +function* getQueryPairs() { + yield ['user', 'abc']; + yield ['query', 'first']; + yield ['query', 'second']; +} +params = new URLSearchParams(getQueryPairs()); +console.log(params.toString()); + // Prints 'user=abc&query=first&query=second' + +// Using a generator function for key-value pairs +function* getSingleQueryPair(idx) { + if (idx === 0) { + yield 'user'; yield 'abc'; + } else if (idx === 1) { + yield 'query'; yield 'first'; + } else { + yield 'query'; yield 'second'; + } +} +params = new URLSearchParams([ + getSingleQueryPair(0), + getSingleQueryPair(1), + getSingleQueryPair(2) +]); +console.log(params.toString()); + // Prints 'user=abc&query=first&query=second' + +// Each key-value pair must have exactly two elements +new URLSearchParams([ + ['user', 'abc', 'error'] +]); + // Throws TypeError: Each query pair must be a name/value tuple +``` #### urlSearchParams.append(name, value) @@ -712,3 +825,5 @@ console.log(myURL.origin); [`url.parse()`]: #url_url_parse_urlstring_parsequerystring_slashesdenotehost [`url.format()`]: #url_url_format_urlobject [Punycode]: https://tools.ietf.org/html/rfc5891#section-4.4 +[`Map`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map +[`array.toString()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString From 1fcac0203fb64d115027fcd42e2f171ee3fb6662 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sun, 29 Jan 2017 01:40:35 -0800 Subject: [PATCH 4/8] Address comments --- doc/api/url.md | 24 ++++--------------- lib/internal/url.js | 3 +-- ...est-whatwg-url-searchparams-constructor.js | 6 +++++ 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/doc/api/url.md b/doc/api/url.md index 54fda577b5d0ff..0c9d3661b5ad88 100755 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -599,8 +599,10 @@ console.log(params.toString()); Instantiate a new `URLSearchParams` object with an iterable map in a way that is similar to [`Map`][]'s constructor. `iterable` can be an Array or any -iterable object. Elements of `iterable` are key-value pairs, and can themselves -be any iterable object. +iterable object. That means `iterable` can be another `URLSearchParams`, in +which case the constructor will simply create a clone of the provided +`URLSearchParams`. Elements of `iterable` are key-value pairs, and can +themselves be any iterable object. Duplicate keys are allowed. @@ -635,24 +637,6 @@ params = new URLSearchParams(getQueryPairs()); console.log(params.toString()); // Prints 'user=abc&query=first&query=second' -// Using a generator function for key-value pairs -function* getSingleQueryPair(idx) { - if (idx === 0) { - yield 'user'; yield 'abc'; - } else if (idx === 1) { - yield 'query'; yield 'first'; - } else { - yield 'query'; yield 'second'; - } -} -params = new URLSearchParams([ - getSingleQueryPair(0), - getSingleQueryPair(1), - getSingleQueryPair(2) -]); -console.log(params.toString()); - // Prints 'user=abc&query=first&query=second' - // Each key-value pair must have exactly two elements new URLSearchParams([ ['user', 'abc', 'error'] diff --git a/lib/internal/url.js b/lib/internal/url.js index 0c71c57acec280..3a7721f290525a 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -648,9 +648,8 @@ function getObjectFromParams(array) { } class URLSearchParams { - constructor(init = '') { + constructor(init) { if (init === null || init === undefined) { - // record this[searchParams] = []; } else if (typeof init === 'object') { const method = init[Symbol.iterator]; diff --git a/test/parallel/test-whatwg-url-searchparams-constructor.js b/test/parallel/test-whatwg-url-searchparams-constructor.js index be2379a0390f31..653b3a024aa4c5 100644 --- a/test/parallel/test-whatwg-url-searchparams-constructor.js +++ b/test/parallel/test-whatwg-url-searchparams-constructor.js @@ -23,6 +23,12 @@ assert.strictEqual(params.toString(), ''); assert.throws(() => URLSearchParams(), TypeError, 'Calling \'URLSearchParams\' without \'new\' should throw.'); +// URLSearchParams constructor, undefined and null as argument +params = new URLSearchParams(undefined); +assert.strictEqual(params.toString(), ''); +params = new URLSearchParams(null); +assert.strictEqual(params.toString(), ''); + // URLSearchParams constructor, empty string as argument params = new URLSearchParams(''); // eslint-disable-next-line no-restricted-properties From 1bcb813b1c7ec02f11c7ea8a99a399783194ebf6 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sun, 29 Jan 2017 01:42:13 -0800 Subject: [PATCH 5/8] Simplify test --- test/parallel/test-whatwg-url-searchparams-constructor.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/parallel/test-whatwg-url-searchparams-constructor.js b/test/parallel/test-whatwg-url-searchparams-constructor.js index 653b3a024aa4c5..cb6ee0c9c04550 100644 --- a/test/parallel/test-whatwg-url-searchparams-constructor.js +++ b/test/parallel/test-whatwg-url-searchparams-constructor.js @@ -168,12 +168,8 @@ assert.throws(() => new URLSearchParams([[1, 2, 3]]), } ].forEach((val) => { const params = new URLSearchParams(val.input); - let i = 0; - for (const param of params) { - assert.deepStrictEqual(param, val.output[i], - `Construct with ${val.name}`); - i++; - } + assert.deepStrictEqual(Array.from(params), val.output, + `Construct with ${val.name}`); }); // Custom [Symbol.iterator] From 28130631bd09aaf7988adcdf1b5b5e7031e15c5a Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sun, 29 Jan 2017 19:41:06 -0800 Subject: [PATCH 6/8] Bring back default parameter --- lib/internal/url.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/internal/url.js b/lib/internal/url.js index 3a7721f290525a..27f6e56861b9fc 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -648,7 +648,11 @@ function getObjectFromParams(array) { } class URLSearchParams { - constructor(init) { + // URL Standard says the default value is '', but as undefined and '' have + // the same result, undefined is used to prevent unnecessary parsing. + // Default parameter is necessary to keep URLSearchParams.length === 0 in + // accordance with Web IDL spec. + constructor(init = undefined) { if (init === null || init === undefined) { this[searchParams] = []; } else if (typeof init === 'object') { From 4fcb60d6e3db01251bd1b308db8b9d5b174b4c6a Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 30 Jan 2017 11:54:14 -0800 Subject: [PATCH 7/8] doc: address comments --- doc/api/url.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/url.md b/doc/api/url.md index 0c9d3661b5ad88..82edd73e4dd583 100755 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -525,8 +525,8 @@ value returned is equivalent to that of `url.href`. ### Class: URLSearchParams The `URLSearchParams` object provides read and write access to the query of a -`URL`. It can also be used standalone with one of the four following -constructors. +`URL`. The `URLSearchParams` class can also be used standalone with one of the +four following constructors. ```js const URL = require('url').URL; @@ -577,7 +577,7 @@ console.log(params.toString()); Instantiate a new `URLSearchParams` object with a query hash map. The key and value of each property of `obj` are always coerced to strings. -Warning: Unlike [`querystring`][] module, duplicate keys in the form of array +*Note*: Unlike [`querystring`][] module, duplicate keys in the form of array values are not allowed. Arrays are stringified using [`array.toString()`][], which simply joins all array elements with commas. From 25a60ad0bb089c2495d2830f7e88d6460dbd6d22 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 30 Jan 2017 16:49:14 -0800 Subject: [PATCH 8/8] doc: add a few more URL and URLSearchParams interop examples --- doc/api/url.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/api/url.md b/doc/api/url.md index 82edd73e4dd583..435bbff9d7a163 100755 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -529,7 +529,8 @@ The `URLSearchParams` object provides read and write access to the query of a four following constructors. ```js -const URL = require('url').URL; +const { URL, URLSearchParams } = require('url'); + const myURL = new URL('https://example.org/?abc=123'); console.log(myURL.searchParams.get('abc')); // Prints 123 @@ -542,6 +543,24 @@ myURL.searchParams.delete('abc'); myURL.searchParams.set('a', 'b'); console.log(myURL.href); // Prints https://example.org/?a=b + +const newSearchParams = new URLSearchParams(myURL.searchParams); +// The above is equivalent to +// const newSearchParams = new URLSearchParams(myURL.search); + +newSearchParams.append('a', 'c'); +console.log(myURL.href); + // Prints https://example.org/?a=b +console.log(newSearchParams.toString()); + // Prints a=b&a=c + +// newSearchParams.toString() is implicitly called +myURL.search = newSearchParams; +console.log(myURL.href); + // Prints https://example.org/?a=b&a=c +newSearchParams.delete('a'); +console.log(myURL.href); + // Prints https://example.org/?a=b&a=c ``` #### Constructor: new URLSearchParams()