Skip to content

Commit

Permalink
Add support for parsing/stringifying fragment identifier (#222)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Mark1626 and sindresorhus committed Jun 6, 2020
1 parent 1ad8bbd commit ce06095
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 7 deletions.
38 changes: 38 additions & 0 deletions index.d.ts
Expand Up @@ -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> {
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -332,13 +359,24 @@ 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'}});
//=> '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'
```
*/
export function stringifyUrl(
Expand Down
29 changes: 24 additions & 5 deletions index.js
Expand Up @@ -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}`;
};
3 changes: 3 additions & 0 deletions index.test-d.ts
Expand Up @@ -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'));
40 changes: 38 additions & 2 deletions readme.md
Expand Up @@ -334,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?)
Expand All @@ -355,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
Expand Down
7 changes: 7 additions & 0 deletions test/parse-url.js
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions test/stringify-url.js
Expand Up @@ -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};

Expand Down

0 comments on commit ce06095

Please sign in to comment.