Skip to content

Commit

Permalink
url: extend url.format to support WHATWG URL
Browse files Browse the repository at this point in the history
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/
```

PR-URL: #10857
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
Reviewed-By: Brian White <mscdex@mscdex.net>
  • Loading branch information
jasnell committed Feb 2, 2017
1 parent a334252 commit c5e9654
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 37 deletions.
60 changes: 30 additions & 30 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -1120,3 +1119,4 @@ exports.domainToASCII = domainToASCII;
exports.domainToUnicode = domainToUnicode;
exports.encodeAuth = encodeAuth;
exports.urlToOptions = urlToOptions;
exports.formatSymbol = kFormat;
17 changes: 10 additions & 7 deletions lib/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
102 changes: 102 additions & 0 deletions test/parallel/test-url-format-whatwg.js
Original file line number Diff line number Diff line change
@@ -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'
);

0 comments on commit c5e9654

Please sign in to comment.