From 889c5f3cd67c04b596b4676c2d261c991cf39ef2 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 17 Jan 2017 09:07:18 -0800 Subject: [PATCH 1/2] url: extend url.format to support WHATWG URL Removes the non-standard options on WHATWG URL toString and extends the existing url.format() API to support customizable serialization of the WHATWG URL object. This does not yet include the documentation updates because the documentation for the new WHATWG URL object has not yet landed. Example: ```js const url = require('url'); const URL = url.URL; const myURL = new URL('http://example.org/?a=b#c'); const str = url.format(myURL, {fragment: false, search: false}); console.log(str); // Prints: http://example.org/ ``` --- lib/internal/url.js | 60 +++++++------- lib/url.js | 17 ++-- test/parallel/test-url-format-whatwg.js | 102 ++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 37 deletions(-) create mode 100644 test/parallel/test-url-format-whatwg.js diff --git a/lib/internal/url.js b/lib/internal/url.js index da4eb07a815c45..dc3c8a7cbdc98b 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -1,13 +1,5 @@ 'use strict'; -function getPunycode() { - try { - return process.binding('icu'); - } catch (err) { - return require('punycode'); - } -} -const punycode = getPunycode(); const util = require('util'); const binding = process.binding('url'); const context = Symbol('context'); @@ -20,6 +12,7 @@ const kScheme = Symbol('scheme'); const kHost = Symbol('host'); const kPort = Symbol('port'); const kDomain = Symbol('domain'); +const kFormat = Symbol('format'); // https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object const IteratorPrototype = Object.getPrototypeOf( @@ -263,18 +256,19 @@ class URL { } Object.defineProperties(URL.prototype, { - toString: { - // https://heycam.github.io/webidl/#es-stringifier - writable: true, - enumerable: true, - configurable: true, + [kFormat]: { + enumerable: false, + configurable: false, // eslint-disable-next-line func-name-matching - value: function toString(options) { - options = options || {}; - const fragment = - options.fragment !== undefined ? - !!options.fragment : true; - const unicode = !!options.unicode; + value: function format(options) { + if (options && typeof options !== 'object') + throw new TypeError('options must be an object'); + options = Object.assign({ + fragment: true, + unicode: false, + search: true, + auth: true + }, options); const ctx = this[context]; var ret; if (this.protocol) @@ -284,28 +278,23 @@ Object.defineProperties(URL.prototype, { const has_username = typeof ctx.username === 'string'; const has_password = typeof ctx.password === 'string' && ctx.password !== ''; - if (has_username || has_password) { + if (options.auth && (has_username || has_password)) { if (has_username) ret += ctx.username; if (has_password) ret += `:${ctx.password}`; ret += '@'; } - if (unicode) { - ret += punycode.toUnicode(this.hostname); - if (this.port !== undefined) - ret += `:${this.port}`; - } else { - ret += this.host; - } + ret += options.unicode ? + domainToUnicode(this.host) : this.host; } else if (ctx.scheme === 'file:') { ret += '//'; } if (this.pathname) ret += this.pathname; - if (typeof ctx.query === 'string') + if (options.search && typeof ctx.query === 'string') ret += `?${ctx.query}`; - if (fragment & typeof ctx.fragment === 'string') + if (options.fragment && typeof ctx.fragment === 'string') ret += `#${ctx.fragment}`; return ret; } @@ -314,11 +303,21 @@ Object.defineProperties(URL.prototype, { configurable: true, value: 'URL' }, + toString: { + // https://heycam.github.io/webidl/#es-stringifier + writable: true, + enumerable: true, + configurable: true, + // eslint-disable-next-line func-name-matching + value: function toString() { + return this[kFormat]({}); + } + }, href: { enumerable: true, configurable: true, get() { - return this.toString(); + return this[kFormat]({}); }, set(input) { parse(this, input); @@ -1120,3 +1119,4 @@ exports.domainToASCII = domainToASCII; exports.domainToUnicode = domainToUnicode; exports.encodeAuth = encodeAuth; exports.urlToOptions = urlToOptions; +exports.formatSymbol = kFormat; diff --git a/lib/url.js b/lib/url.js index e4ced8e8601cc3..2b7dd6e5321977 100644 --- a/lib/url.js +++ b/lib/url.js @@ -538,19 +538,22 @@ function autoEscapeStr(rest) { } // format a parsed object into a url string -function urlFormat(obj) { +function urlFormat(obj, options) { // ensure it's an object, and not a string url. // If it's an obj, this is a no-op. // this way, you can call url_format() on strings // to clean up potentially wonky urls. - if (typeof obj === 'string') obj = urlParse(obj); - - else if (typeof obj !== 'object' || obj === null) + if (typeof obj === 'string') { + obj = urlParse(obj); + } else if (typeof obj !== 'object' || obj === null) { throw new TypeError('Parameter "urlObj" must be an object, not ' + obj === null ? 'null' : typeof obj); - - else if (!(obj instanceof Url)) return Url.prototype.format.call(obj); - + } else if (!(obj instanceof Url)) { + var format = obj[internalUrl.formatSymbol]; + return format ? + format.call(obj, options) : + Url.prototype.format.call(obj); + } return obj.format(); } diff --git a/test/parallel/test-url-format-whatwg.js b/test/parallel/test-url-format-whatwg.js new file mode 100644 index 00000000000000..507d3f8419d73e --- /dev/null +++ b/test/parallel/test-url-format-whatwg.js @@ -0,0 +1,102 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const url = require('url'); +const URL = url.URL; + +const myURL = new URL('http://xn--lck1c3crb1723bpq4a.com/a?a=b#c'); + +assert.strictEqual( + url.format(myURL), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +const errreg = /^TypeError: options must be an object$/; +assert.throws(() => url.format(myURL, true), errreg); +assert.throws(() => url.format(myURL, 1), errreg); +assert.throws(() => url.format(myURL, 'test'), errreg); +assert.throws(() => url.format(myURL, Infinity), errreg); + +// Any falsy value other than undefined will be treated as false. +// Any truthy value will be treated as true. + +assert.strictEqual( + url.format(myURL, {fragment: false}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b' +); + +assert.strictEqual( + url.format(myURL, {fragment: ''}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b' +); + +assert.strictEqual( + url.format(myURL, {fragment: 0}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b' +); + +assert.strictEqual( + url.format(myURL, {fragment: 1}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {fragment: {}}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {search: false}), + 'http://xn--lck1c3crb1723bpq4a.com/a#c' +); + +assert.strictEqual( + url.format(myURL, {search: ''}), + 'http://xn--lck1c3crb1723bpq4a.com/a#c' +); + +assert.strictEqual( + url.format(myURL, {search: 0}), + 'http://xn--lck1c3crb1723bpq4a.com/a#c' +); + +assert.strictEqual( + url.format(myURL, {search: 1}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {search: {}}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {unicode: true}), + 'http://理容ナカムラ.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {unicode: 1}), + 'http://理容ナカムラ.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {unicode: {}}), + 'http://理容ナカムラ.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {unicode: false}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); + +assert.strictEqual( + url.format(myURL, {unicode: 0}), + 'http://xn--lck1c3crb1723bpq4a.com/a?a=b#c' +); From d1d7b028d9cc5a7affb11ec02b28823db3f1844c Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 24 Jan 2017 12:38:30 -0800 Subject: [PATCH 2/2] doc: add documentation for url.format(URL[, options]); --- doc/api/url.md | 42 ++++++++++++++++++++++++++++++++++++++++ tools/doc/type-parser.js | 3 ++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/doc/api/url.md b/doc/api/url.md index 435bbff9d7a163..4a9f0361d196e9 100755 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -198,6 +198,47 @@ The formatting process operates as follows: string, an [`Error`][] is thrown. * `result` is returned. +## url.format(URL[, options]) + +> Stability: 1 - Experimental + +* `URL` {URL} A [WHATWG URL][] object +* `options` {Object} + * `auth` {Boolean} `true` if the serialized URL string should include the + username and password, `false` otherwise. Defaults to `true`. + * `fragment` {Boolean} `true` if the serialized URL string should include the + fragment, `false` otherwise. Defaults to `true`. + * `search` {Boolean} `true` if the serialized URL string should include the + search query, `false` otherwise. Defaults to `true`. + * `unicode` (Boolean) `true` if Unicode characters appearing in the host + component of the URL string should be encoded directly as opposed to being + Punycode encoded. Defaults to `false`. + +Returns a customizable serialization of a URL String representation of a +[WHATWG URL][] object. + +The URL object has both a `toString()` method and `href` property that return +string serializations of the URL. These are not, however, customizable in +any way. The `url.format(URL[, options])` method allows for basic customization +of the output. + +For example: + +```js +const myURL = new URL('https://a:b@你好你好?abc#foo'); + +console.log(myURL.href); + // Prints https://a:b@xn--6qqa088eba/?abc#foo + +console.log(myURL.toString()); + // Prints https://a:b@xn--6qqa088eba/?abc#foo + +console.log(url.format(myURL, {fragment: false, unicode: true, auth: false})); + // Prints 'https://你好你好?abc' +``` + +*Note*: This variation of the `url.format()` method is currently considered to +be experimental. ## url.parse(urlString[, parseQueryString[, slashesDenoteHost]])