uri.ts 18.1 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { isWindows } from 'vs/base/common/platform';
7
import { CharCode } from 'vs/base/common/charCode';
8

9 10 11 12
const _schemePattern = /^\w[\w\d+.-]*$/;
const _singleSlashStart = /^\//;
const _doubleSlashStart = /^\/\//;

13 14 15 16 17 18 19 20 21 22 23
let _throwOnMissingSchema: boolean = true;

/**
 * @internal
 */
export function setUriThrowOnMissingScheme(value: boolean): boolean {
	const old = _throwOnMissingSchema;
	_throwOnMissingSchema = value;
	return old;
}

24
function _validateUri(ret: URI): void {
25

26 27
	// scheme, must be set
	if (!ret.scheme) {
28 29 30 31 32
		if (_throwOnMissingSchema) {
			throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`);
		} else {
			console.warn(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`);
		}
33
	}
34

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
	// scheme, https://tools.ietf.org/html/rfc3986#section-3.1
	// ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
	if (ret.scheme && !_schemePattern.test(ret.scheme)) {
		throw new Error('[UriError]: Scheme contains illegal characters.');
	}

	// path, http://tools.ietf.org/html/rfc3986#section-3.3
	// If a URI contains an authority component, then the path component
	// must either be empty or begin with a slash ("/") character.  If a URI
	// does not contain an authority component, then the path cannot begin
	// with two slash characters ("//").
	if (ret.path) {
		if (ret.authority) {
			if (!_singleSlashStart.test(ret.path)) {
				throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character');
			}
		} else {
			if (_doubleSlashStart.test(ret.path)) {
				throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")');
			}
		}
	}
}

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
// implements a bit of https://tools.ietf.org/html/rfc3986#section-5
function _referenceResolution(scheme: string, path: string): string {

	// the slash-character is our 'default base' as we don't
	// support constructing URIs relative to other URIs. This
	// also means that we alter and potentially break paths.
	// see https://tools.ietf.org/html/rfc3986#section-5.1.4
	switch (scheme) {
		case 'https':
		case 'http':
		case 'file':
			if (!path) {
				path = _slash;
			} else if (path[0] !== _slash) {
				path = _slash + path;
			}
			break;
	}
	return path;
}

80 81 82 83
const _empty = '';
const _slash = '/';
const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;

E
Erich Gamma 已提交
84 85
/**
 * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
86
 * This class is a simple parser which creates the basic component parts
E
Erich Gamma 已提交
87 88 89
 * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
 * and encoding.
 *
90
 *       foo://example.com:8042/over/there?name=ferret#nose
E
Erich Gamma 已提交
91 92 93 94 95 96 97
 *       \_/   \______________/\_________/ \_________/ \__/
 *        |           |            |            |        |
 *     scheme     authority       path        query   fragment
 *        |   _____________________|__
 *       / \ /                        \
 *       urn:example:animal:ferret:nose
 */
98
export class URI implements UriComponents {
E
Erich Gamma 已提交
99

100 101 102 103 104 105 106 107 108 109 110 111 112 113
	static isUri(thing: any): thing is URI {
		if (thing instanceof URI) {
			return true;
		}
		if (!thing) {
			return false;
		}
		return typeof (<URI>thing).authority === 'string'
			&& typeof (<URI>thing).fragment === 'string'
			&& typeof (<URI>thing).path === 'string'
			&& typeof (<URI>thing).query === 'string'
			&& typeof (<URI>thing).scheme === 'string';
	}

E
Erich Gamma 已提交
114 115 116 117
	/**
	 * scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'.
	 * The part before the first colon.
	 */
J
Johannes Rieken 已提交
118
	readonly scheme: string;
E
Erich Gamma 已提交
119 120 121 122 123

	/**
	 * authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'.
	 * The part between the first double slashes and the next slash.
	 */
J
Johannes Rieken 已提交
124
	readonly authority: string;
E
Erich Gamma 已提交
125 126 127 128

	/**
	 * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'.
	 */
J
Johannes Rieken 已提交
129
	readonly path: string;
E
Erich Gamma 已提交
130 131 132 133

	/**
	 * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'.
	 */
J
Johannes Rieken 已提交
134
	readonly query: string;
E
Erich Gamma 已提交
135 136 137 138

	/**
	 * fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'.
	 */
J
Johannes Rieken 已提交
139 140 141 142 143
	readonly fragment: string;

	/**
	 * @internal
	 */
144
	protected constructor(scheme: string, authority?: string, path?: string, query?: string, fragment?: string);
J
Johannes Rieken 已提交
145

J
Johannes Rieken 已提交
146 147 148 149
	/**
	 * @internal
	 */
	protected constructor(components: UriComponents);
J
Johannes Rieken 已提交
150

J
Johannes Rieken 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
	/**
	 * @internal
	 */
	protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string) {

		if (typeof schemeOrData === 'object') {
			this.scheme = schemeOrData.scheme || _empty;
			this.authority = schemeOrData.authority || _empty;
			this.path = schemeOrData.path || _empty;
			this.query = schemeOrData.query || _empty;
			this.fragment = schemeOrData.fragment || _empty;
			// no validation because it's this URI
			// that creates uri components.
			// _validateUri(this);
		} else {
			this.scheme = schemeOrData || _empty;
			this.authority = authority || _empty;
168
			this.path = _referenceResolution(this.scheme, path || _empty);
J
Johannes Rieken 已提交
169 170
			this.query = query || _empty;
			this.fragment = fragment || _empty;
171

J
Johannes Rieken 已提交
172 173
			_validateUri(this);
		}
E
Erich Gamma 已提交
174 175 176 177 178
	}

	// ---- filesystem path -----------------------

	/**
P
Pascal Borreli 已提交
179
	 * Returns a string representing the corresponding file system path of this URI.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
	 * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the
	 * platform specific path separator.
	 *
	 * * Will *not* validate the path for invalid characters and semantics.
	 * * Will *not* look at the scheme of this URI.
	 * * The result shall *not* be used for display purposes but for accessing a file on disk.
	 *
	 *
	 * The *difference* to `URI#path` is the use of the platform specific separator and the handling
	 * of UNC paths. See the below sample of a file-uri with an authority (UNC path).
	 *
	 * ```ts
		const u = URI.parse('file://server/c$/folder/file.txt')
		u.authority === 'server'
		u.path === '/shares/c$/file.txt'
		u.fsPath === '\\server\c$\folder\file.txt'
	```
	 *
	 * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
	 * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working
	 * with URIs that represent files on disk (`file` scheme).
E
Erich Gamma 已提交
201
	 */
J
Johannes Rieken 已提交
202
	get fsPath(): string {
203 204 205
		// if (this.scheme !== 'file') {
		// 	console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`);
		// }
206
		return _makeFsPath(this);
E
Erich Gamma 已提交
207 208 209 210
	}

	// ---- modify to new -------------------------

211
	public with(change: { scheme?: string; authority?: string | null; path?: string | null; query?: string | null; fragment?: string | null }): URI {
212 213 214 215 216

		if (!change) {
			return this;
		}

217
		let { scheme, authority, path, query, fragment } = change;
218 219
		if (scheme === void 0) {
			scheme = this.scheme;
J
Johannes Rieken 已提交
220
		} else if (scheme === null) {
J
Johannes Rieken 已提交
221
			scheme = _empty;
222 223 224
		}
		if (authority === void 0) {
			authority = this.authority;
J
Johannes Rieken 已提交
225
		} else if (authority === null) {
J
Johannes Rieken 已提交
226
			authority = _empty;
227 228 229
		}
		if (path === void 0) {
			path = this.path;
J
Johannes Rieken 已提交
230
		} else if (path === null) {
J
Johannes Rieken 已提交
231
			path = _empty;
232 233 234
		}
		if (query === void 0) {
			query = this.query;
J
Johannes Rieken 已提交
235
		} else if (query === null) {
J
Johannes Rieken 已提交
236
			query = _empty;
237 238 239
		}
		if (fragment === void 0) {
			fragment = this.fragment;
J
Johannes Rieken 已提交
240
		} else if (fragment === null) {
J
Johannes Rieken 已提交
241
			fragment = _empty;
242
		}
243 244 245 246 247 248 249 250 251 252

		if (scheme === this.scheme
			&& authority === this.authority
			&& path === this.path
			&& query === this.query
			&& fragment === this.fragment) {

			return this;
		}

253
		return new _URI(scheme, authority, path, query, fragment);
E
Erich Gamma 已提交
254 255 256 257
	}

	// ---- parse & validate ------------------------

258 259 260 261 262 263
	/**
	 * Creates a new URI from a string, e.g. `http://www.msft.com/some/path`,
	 * `file:///usr/home`, or `scheme:with/path`.
	 *
	 * @param value A string which represents an URI (see `URI#toString`).
	 */
E
Erich Gamma 已提交
264
	public static parse(value: string): URI {
265
		const match = _regexp.exec(value);
J
Johannes Rieken 已提交
266
		if (!match) {
267
			return new _URI(_empty, _empty, _empty, _empty, _empty);
J
Johannes Rieken 已提交
268
		}
269
		return new _URI(
270 271 272 273 274
			match[2] || _empty,
			decodeURIComponent(match[4] || _empty),
			decodeURIComponent(match[5] || _empty),
			decodeURIComponent(match[7] || _empty),
			decodeURIComponent(match[9] || _empty),
J
Johannes Rieken 已提交
275
		);
E
Erich Gamma 已提交
276 277
	}

278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
	/**
	 * Creates a new URI from a file system path, e.g. `c:\my\files`,
	 * `/usr/home`, or `\\server\share\some\path`.
	 *
	 * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
	 * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
	 * `URI.parse('file://' + path)` because the path might contain characters that are
	 * interpreted (# and ?). See the following sample:
	 * ```ts
	const good = URI.file('/coding/c#/project1');
	good.scheme === 'file';
	good.path === '/coding/c#/project1';
	good.fragment === '';
	const bad = URI.parse('file://' + '/coding/c#/project1');
	bad.scheme === 'file';
	bad.path === '/coding/c'; // path is now broken
	bad.fragment === '/project1';
	```
	 *
	 * @param path A file system path (see `URI#fsPath`)
	 */
E
Erich Gamma 已提交
299 300
	public static file(path: string): URI {

301
		let authority = _empty;
302

303
		// normalize to fwd-slashes on windows,
A
typo  
Andre Weinand 已提交
304
		// on other systems bwd-slashes are valid
305
		// filename character, eg /f\oo/ba\r.txt
306
		if (isWindows) {
307
			path = path.replace(/\\/g, _slash);
308
		}
309 310 311

		// check for authority as used in UNC shares
		// or use the path as given
J
Johannes Rieken 已提交
312
		if (path[0] === _slash && path[1] === _slash) {
313
			let idx = path.indexOf(_slash, 2);
314
			if (idx === -1) {
J
Johannes Rieken 已提交
315
				authority = path.substring(2);
J
Johannes Rieken 已提交
316
				path = _slash;
317
			} else {
J
Johannes Rieken 已提交
318
				authority = path.substring(2, idx);
J
Johannes Rieken 已提交
319
				path = path.substring(idx) || _slash;
320 321 322
			}
		}

323
		return new _URI('file', authority, path, _empty, _empty);
E
Erich Gamma 已提交
324 325
	}

326
	public static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI {
327
		return new _URI(
J
Johannes Rieken 已提交
328 329 330 331 332 333
			components.scheme,
			components.authority,
			components.path,
			components.query,
			components.fragment,
		);
E
Erich Gamma 已提交
334 335 336 337
	}

	// ---- printing/externalize ---------------------------

338
	/**
D
Daksh 已提交
339
	 * Creates a string presentation for this URI. It's guaranteed that calling
340 341 342 343 344 345
	 * `URI.parse` with the result of this function creates an URI which is equal
	 * to this URI.
	 *
	 * * The result shall *not* be used for display purposes but for externalization or transport.
	 * * The result will be encoded using the percentage encoding and encoding happens mostly
	 * ignore the scheme-specific encoding rules.
346
	 *
347
	 * @param skipEncoding Do not encode the result, default is `false`
348
	 */
349 350
	public toString(skipEncoding: boolean = false): string {
		return _asFormatted(this, skipEncoding);
351 352
	}

J
Johannes Rieken 已提交
353
	public toJSON(): object {
354
		return this;
355 356
	}

J
Johannes Rieken 已提交
357
	static revive(data: UriComponents | any): URI {
J
Johannes Rieken 已提交
358 359 360 361 362 363
		if (!data) {
			return data;
		} else if (data instanceof URI) {
			return data;
		} else {
			let result = new _URI(data);
364 365 366 367
			if ((<UriState>data).$mid === 100) {
				result._fsPath = (<UriState>data).fsPath;
				result._formatted = (<UriState>data).external;
			}
J
Johannes Rieken 已提交
368 369
			return result;
		}
370 371 372
	}
}

J
Johannes Rieken 已提交
373
export interface UriComponents {
374 375 376 377 378 379 380 381
	scheme: string;
	authority: string;
	path: string;
	query: string;
	fragment: string;
}

interface UriState extends UriComponents {
382
	$mid: 100;
383 384 385 386
	fsPath: string;
	external: string;
}

387

388 389
// tslint:disable-next-line:class-name
class _URI extends URI {
390

391 392
	_formatted: string | null = null;
	_fsPath: string | null = null;
393 394 395

	get fsPath(): string {
		if (!this._fsPath) {
396
			this._fsPath = _makeFsPath(this);
397 398 399 400
		}
		return this._fsPath;
	}

401 402
	public toString(skipEncoding: boolean = false): string {
		if (!skipEncoding) {
403
			if (!this._formatted) {
404
				this._formatted = _asFormatted(this, false);
405 406 407 408
			}
			return this._formatted;
		} else {
			// we don't cache that
409
			return _asFormatted(this, true);
J
Johannes Rieken 已提交
410 411
		}
	}
412 413 414

	toJSON(): object {
		const res = <UriState>{
415
			$mid: 100
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
		};
		// cached state
		if (this._fsPath) {
			res.fsPath = this._fsPath;
		}
		if (this._formatted) {
			res.external = this._formatted;
		}
		// uri components
		if (this.path) {
			res.path = this.path;
		}
		if (this.scheme) {
			res.scheme = this.scheme;
		}
		if (this.authority) {
			res.authority = this.authority;
		}
		if (this.query) {
			res.query = this.query;
		}
		if (this.fragment) {
			res.fragment = this.fragment;
		}
		return res;
	}
442 443
}

444
// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2
445
const encodeTable: { [ch: number]: string } = {
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
	[CharCode.Colon]: '%3A', // gen-delims
	[CharCode.Slash]: '%2F',
	[CharCode.QuestionMark]: '%3F',
	[CharCode.Hash]: '%23',
	[CharCode.OpenSquareBracket]: '%5B',
	[CharCode.CloseSquareBracket]: '%5D',
	[CharCode.AtSign]: '%40',

	[CharCode.ExclamationMark]: '%21', // sub-delims
	[CharCode.DollarSign]: '%24',
	[CharCode.Ampersand]: '%26',
	[CharCode.SingleQuote]: '%27',
	[CharCode.OpenParen]: '%28',
	[CharCode.CloseParen]: '%29',
	[CharCode.Asterisk]: '%2A',
	[CharCode.Plus]: '%2B',
	[CharCode.Comma]: '%2C',
	[CharCode.Semicolon]: '%3B',
	[CharCode.Equals]: '%3D',

	[CharCode.Space]: '%20',
};

J
Johannes Rieken 已提交
469 470
function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): string {
	let res: string = undefined;
471 472
	let nativeEncodePos = -1;

J
Johannes Rieken 已提交
473
	for (let pos = 0; pos < uriComponent.length; pos++) {
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
		let code = uriComponent.charCodeAt(pos);

		// unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
		if (
			(code >= CharCode.a && code <= CharCode.z)
			|| (code >= CharCode.A && code <= CharCode.Z)
			|| (code >= CharCode.Digit0 && code <= CharCode.Digit9)
			|| code === CharCode.Dash
			|| code === CharCode.Period
			|| code === CharCode.Underline
			|| code === CharCode.Tilde
			|| (allowSlash && code === CharCode.Slash)
		) {
			// check if we are delaying native encode
			if (nativeEncodePos !== -1) {
				res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
				nativeEncodePos = -1;
			}
			// check if we write into a new string (by default we try to return the param)
			if (res !== undefined) {
				res += uriComponent.charAt(pos);
			}

		} else {
			// encoding needed, we need to allocate a new string
			if (res === undefined) {
				res = uriComponent.substr(0, pos);
			}

			// check with default table first
			let escaped = encodeTable[code];
			if (escaped !== undefined) {

				// check if we are delaying native encode
				if (nativeEncodePos !== -1) {
					res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
					nativeEncodePos = -1;
				}

				// append escaped variant to result
				res += escaped;

			} else if (nativeEncodePos === -1) {
				// use native encode only when needed
				nativeEncodePos = pos;
			}
		}
	}

	if (nativeEncodePos !== -1) {
		res += encodeURIComponent(uriComponent.substring(nativeEncodePos));
	}

	return res !== undefined ? res : uriComponent;
}

function encodeURIComponentMinimal(path: string): string {
531
	let res: string | undefined = undefined;
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
	for (let pos = 0; pos < path.length; pos++) {
		let code = path.charCodeAt(pos);
		if (code === CharCode.Hash || code === CharCode.QuestionMark) {
			if (res === undefined) {
				res = path.substr(0, pos);
			}
			res += encodeTable[code];
		} else {
			if (res !== undefined) {
				res += path[pos];
			}
		}
	}
	return res !== undefined ? res : path;
}
J
Johannes Rieken 已提交
547

548 549 550 551 552 553 554
/**
 * Compute `fsPath` for the given uri
 * @param uri
 */
function _makeFsPath(uri: URI): string {

	let value: string;
555
	if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
556 557
		// unc path: file://shares/c$/far/boo
		value = `//${uri.authority}${uri.path}`;
J
Johannes Rieken 已提交
558 559 560 561 562
	} else if (
		uri.path.charCodeAt(0) === CharCode.Slash
		&& (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)
		&& uri.path.charCodeAt(2) === CharCode.Colon
	) {
563 564 565 566 567 568
		// windows drive letter: file:///c:/far/boo
		value = uri.path[1].toLowerCase() + uri.path.substr(2);
	} else {
		// other path
		value = uri.path;
	}
569
	if (isWindows) {
570 571 572 573
		value = value.replace(/\//g, '\\');
	}
	return value;
}
574

575 576 577
/**
 * Create the external version of a uri
 */
578
function _asFormatted(uri: URI, skipEncoding: boolean): string {
579

580
	const encoder = !skipEncoding
581 582
		? encodeURIComponentFast
		: encodeURIComponentMinimal;
E
Erich Gamma 已提交
583

584
	let res = '';
585 586
	let { scheme, authority, path, query, fragment } = uri;
	if (scheme) {
587 588
		res += scheme;
		res += ':';
589 590
	}
	if (authority || scheme === 'file') {
591 592
		res += _slash;
		res += _slash;
593 594 595 596
	}
	if (authority) {
		let idx = authority.indexOf('@');
		if (idx !== -1) {
597
			// <user>@<auth>
598 599 600
			const userinfo = authority.substr(0, idx);
			authority = authority.substr(idx + 1);
			idx = userinfo.indexOf(':');
J
Johannes Rieken 已提交
601
			if (idx === -1) {
602
				res += encoder(userinfo, false);
J
Johannes Rieken 已提交
603
			} else {
604
				// <user>:<pass>@<auth>
605
				res += encoder(userinfo.substr(0, idx), false);
606
				res += ':';
607
				res += encoder(userinfo.substr(idx + 1), false);
E
Erich Gamma 已提交
608
			}
609
			res += '@';
J
Johannes Rieken 已提交
610
		}
611 612 613
		authority = authority.toLowerCase();
		idx = authority.indexOf(':');
		if (idx === -1) {
614
			res += encoder(authority, false);
615
		} else {
616
			// <auth>:<port>
617
			res += encoder(authority.substr(0, idx), false);
618
			res += authority.substr(idx);
J
Johannes Rieken 已提交
619
		}
620 621 622
	}
	if (path) {
		// lower-case windows drive letters in /C:/fff or C:/fff
623 624 625 626
		if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) {
			let code = path.charCodeAt(1);
			if (code >= CharCode.A && code <= CharCode.Z) {
				path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
627
			}
628 629 630 631
		} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
			let code = path.charCodeAt(0);
			if (code >= CharCode.A && code <= CharCode.Z) {
				path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
632
			}
633
		}
634
		// encode the rest of the path
J
Johannes Rieken 已提交
635
		res += encoder(path, true);
636 637
	}
	if (query) {
638
		res += '?';
639
		res += encoder(query, false);
640 641
	}
	if (fragment) {
642
		res += '#';
J
Johannes Rieken 已提交
643
		res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment;
E
Erich Gamma 已提交
644
	}
645
	return res;
E
Erich Gamma 已提交
646
}