Repository: sindresorhus/query-string (js)

Issue:
Support hash
```javascript
queryString.parseUrl('https://www.omg.com/page#param1&param2');
```
Result without hash...
```javascript
{​
    query: Object {  }
​    url: "https://www.omg.com/page"
}
```

Pull request description:
Add support for parsing/stringifying fragment identifier
Fixes #220 

Add a new option `parseFragment`, which will provide the hash in the `parseURL` result. The new option is to prevent it from becoming a breaking change

Seems `Fragment Identifier` is the technical jargon for it, let me know if it's ok or hash seems like a better choice

cc\ @sindresorhus 

Patch:
diff --git a/.travis.yml b/.travis.yml
index e155464..81f3378 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,6 @@
 language: node_js
 node_js:
+  - '14'
   - '12'
   - '10'
   - '8'
diff --git a/index.d.ts b/index.d.ts
index d5012b4..b3a3c99 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -121,6 +121,21 @@ export interface ParseOptions {
 	```
 	*/
 	readonly parseBooleans?: boolean;
+	
+	/**
+	Parse the fragment identifier from the URL and add it to result object.
+
+	@default false
+
+	@example
+	```
+	import queryString = require('query-string');
+
+	queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true});
+	//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'}
+	```
+	*/
+	readonly parseFragmentIdentifier?: boolean;
 }
 
 export interface ParsedQuery<T = string> {
@@ -142,11 +157,20 @@ export function parse(query: string, options?: ParseOptions): ParsedQuery;
 export interface ParsedUrl {
 	readonly url: string;
 	readonly query: ParsedQuery;
+
+	/**
+	The fragment identifier of the URL.
+
+	Present when the `parseFragmentIdentifier` option is `true`.
+	*/
+	readonly fragmentIdentifier?: string;
 }
 
 /**
 Extract the URL and the query string as an object.
 
+If the `parseFragmentIdentifier` option is `true`, the object will also contain a `fragmentIdentifier` property.
+
 @param url - The URL to parse.
 
 @example
@@ -155,6 +179,9 @@ import queryString = require('query-string');
 
 queryString.parseUrl('https://foo.bar?foo=bar');
 //=> {url: 'https://foo.bar', query: {foo: 'bar'}}
+
+queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true});
+//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'}
 ```
 */
 export function parseUrl(url: string, options?: ParseOptions): ParsedUrl;
@@ -298,7 +325,7 @@ export interface StringifyOptions {
 	});
 	//=> 'a=1&d=4'
 	```
-	
+
 	@example
 	```
 	import queryString = require('query-string');
@@ -332,6 +359,8 @@ Stringify an object into a URL with a query string and sorting the keys. The inv
 
 Query items in the `query` property overrides queries in the `url` property.
 
+The `fragmentIdentifier` property overrides the fragment identifier in the `url` property.
+
 @example
 ```
 queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar'}});
@@ -339,6 +368,15 @@ queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar'}});
 
 queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}});
 //=> 'https://foo.bar?foo=bar'
+
+queryString.stringifyUrl({
+	url: 'https://foo.bar',
+	query: {
+		top: 'foo'
+	},
+	fragmentIdentifier: 'bar'
+});
+//=> 'https://foo.bar?top=foo#bar'
 ```
 */
 export function stringifyUrl(
diff --git a/index.js b/index.js
index 0b57b3e..f8ca0df 100644
--- a/index.js
+++ b/index.js
@@ -338,22 +338,41 @@ exports.stringify = (object, options) => {
 };
 
 exports.parseUrl = (input, options) => {
-	return {
-		url: removeHash(input).split('?')[0] || '',
-		query: parse(extract(input), options)
-	};
+	options = Object.assign({
+		decode: true
+	}, options);
+
+	const [url, hash] = splitOnFirst(input, '#');
+
+	return Object.assign(
+		{
+			url: url.split('?')[0] || '',
+			query: parse(extract(input), options)
+		},
+		options && options.parseFragmentIdentifier && hash ? {fragmentIdentifier: decode(hash, options)} : {}
+	);
 };
 
 exports.stringifyUrl = (input, options) => {
+	options = Object.assign({
+		encode: true,
+		strict: true
+	}, options);
+
 	const url = removeHash(input.url).split('?')[0] || '';
 	const queryFromUrl = exports.extract(input.url);
 	const parsedQueryFromUrl = exports.parse(queryFromUrl);
-	const hash = getHash(input.url);
+
 	const query = Object.assign(parsedQueryFromUrl, input.query);
 	let queryString = exports.stringify(query, options);
 	if (queryString) {
 		queryString = `?${queryString}`;
 	}
 
+	let hash = getHash(input.url);
+	if (input.fragmentIdentifier) {
+		hash = `#${encode(input.fragmentIdentifier, options)}`;
+	}
+
 	return `${url}${queryString}${hash}`;
 };
diff --git a/license b/license
index e7af2f7..e464bf7 100644
--- a/license
+++ b/license
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
+Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (http://sindresorhus.com)
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
diff --git a/package.json b/package.json
index ac77367..f7ec022 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
 	"author": {
 		"name": "Sindre Sorhus",
 		"email": "sindresorhus@gmail.com",
-		"url": "sindresorhus.com"
+		"url": "https://sindresorhus.com"
 	},
 	"engines": {
 		"node": ">=6"
diff --git a/readme.md b/readme.md
index 75a1d3f..46e58b5 100644
--- a/readme.md
+++ b/readme.md
@@ -1,4 +1,4 @@
-# query-string [![Build Status](https://travis-ci.org/sindresorhus/query-string.svg?branch=master)](https://travis-ci.org/sindresorhus/query-string) [![](https://badgen.net/bundlephobia/minzip/query-string)](https://bundlephobia.com/result?p=query-string)
+# query-string [![Build Status](https://travis-ci.com/sindresorhus/query-string.svg?branch=master)](https://travis-ci.com/github/sindresorhus/query-string) [![](https://badgen.net/bundlephobia/minzip/query-string)](https://bundlephobia.com/result?p=query-string)
 
 > Parse and stringify URL [query strings](https://en.wikipedia.org/wiki/Query_string)
 
@@ -16,14 +16,9 @@
 		<sup>Special thanks to:</sup>
 		<br>
 		<br>
-		<a href="https://github.com/botpress/botpress">
-			<img src="https://sindresorhus.com/assets/thanks/botpress-logo.svg" width="260" alt="Botpress">
+		<a href="https://standardresume.co">
+			<img src="https://sindresorhus.com/assets/thanks/standard-resume-logo.svg" width="200"/>
 		</a>
-		<br>
-		<sub><b>Botpress is an open-source conversational assistant creation platform.</b></sub>
-		<br>
-		<sub>They <a href="https://github.com/botpress/botpress/blob/master/.github/CONTRIBUTING.md">welcome contributions</a> from anyone, whether you're into machine learning,<br>want to get started in open-source, or just have an improvement idea.</sub>
-		<br>
 	</p>
 </div>
 
@@ -339,15 +334,40 @@ Note: This behaviour can be changed with the `skipNull` option.
 
 Extract the URL and the query string as an object.
 
-The `options` are the same as for `.parse()`.
-
 Returns an object with a `url` and `query` property.
 
+If the `parseFragmentIdentifier` option is `true`, the object will also contain a `fragmentIdentifier` property.
+
 ```js
 const queryString = require('query-string');
 
 queryString.parseUrl('https://foo.bar?foo=bar');
 //=> {url: 'https://foo.bar', query: {foo: 'bar'}}
+
+queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true});
+//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'}
+```
+
+#### options
+
+Type: `object`
+
+The options are the same as for `.parse()`.
+
+Extra options are as below.
+
+##### parseFragmentIdentifier
+
+Parse the fragment identifier from the URL.
+
+Type: `boolean`\
+Default: `false`
+
+```js
+const queryString = require('query-string');
+
+queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true});
+//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'}
 ```
 
 ### .stringifyUrl(object, options?)
@@ -360,12 +380,23 @@ Returns a string with the URL and a query string.
 
 Query items in the `query` property overrides queries in the `url` property.
 
+The `fragmentIdentifier` property overrides the fragment identifier in the `url` property.
+
 ```js
 queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar'}});
 //=> 'https://foo.bar?foo=bar'
 
 queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}});
 //=> 'https://foo.bar?foo=bar'
+
+queryString.stringifyUrl({
+	url: 'https://foo.bar',
+	query: {
+		top: 'foo'
+	},
+	fragmentIdentifier: 'bar'
+});
+//=> 'https://foo.bar?top=foo#bar'
 ```
 
 #### object


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 19a6817..a7563e8 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -87,6 +87,9 @@ expectType<queryString.ParsedUrl>(
 expectType<queryString.ParsedUrl>(
 	queryString.parseUrl('?foo=true', {parseBooleans: true})
 );
+expectType<queryString.ParsedUrl>(
+	queryString.parseUrl('?foo=true#bar', {parseFragmentIdentifier: true})
+);
 
 // Extract
 expectType<string>(queryString.extract('http://foo.bar/?abc=def&hij=klm'));
diff --git a/test/parse-url.js b/test/parse-url.js
index a85e88e..720333c 100644
--- a/test/parse-url.js
+++ b/test/parse-url.js
@@ -18,6 +18,13 @@ test('handles strings with query string that contain =', t => {
 	t.deepEqual(queryString.parseUrl('https://foo.bar?foo=bar=&foo=baz='), {url: 'https://foo.bar', query: {foo: ['bar=', 'baz=']}});
 });
 
+test('handles strings with fragment identifier', t => {
+	t.deepEqual(queryString.parseUrl('https://foo.bar?top=foo#bar', {parseFragmentIdentifier: true}), {url: 'https://foo.bar', query: {top: 'foo'}, fragmentIdentifier: 'bar'});
+	t.deepEqual(queryString.parseUrl('https://foo.bar?foo=bar&foo=baz#top', {parseFragmentIdentifier: true}), {url: 'https://foo.bar', query: {foo: ['bar', 'baz']}, fragmentIdentifier: 'top'});
+	t.deepEqual(queryString.parseUrl('https://foo.bar/#top', {parseFragmentIdentifier: true}), {url: 'https://foo.bar/', query: {}, fragmentIdentifier: 'top'});
+	t.deepEqual(queryString.parseUrl('https://foo.bar/#st%C3%A5le', {parseFragmentIdentifier: true}), {url: 'https://foo.bar/', query: {}, fragmentIdentifier: 'ståle'});
+});
+
 test('throws for invalid values', t => {
 	t.throws(() => {
 		queryString.parseUrl(null);
diff --git a/test/stringify-url.js b/test/stringify-url.js
index 933af7a..98055cc 100644
--- a/test/stringify-url.js
+++ b/test/stringify-url.js
@@ -19,6 +19,15 @@ test('stringify URL with a query string', t => {
 	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}}), 'https://foo.bar?foo=bar');
 });
 
+test('stringify URL with fragment identifier', t => {
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {top: 'foo'}, fragmentIdentifier: 'bar'}), 'https://foo.bar?top=foo#bar');
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: ['bar', 'baz']}, fragmentIdentifier: 'top'}), 'https://foo.bar?foo=bar&foo=baz#top');
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/', query: {}, fragmentIdentifier: 'top'}), 'https://foo.bar/#top');
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/#abc', query: {}, fragmentIdentifier: 'top'}), 'https://foo.bar/#top');
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {}}), 'https://foo.bar');
+	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {}, fragmentIdentifier: 'foo bar'}), 'https://foo.bar#foo%20bar');
+});
+
 test('skipEmptyString:: stringify URL with a query string', t => {
 	const config = {skipEmptyString: true};