Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/query-string
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 3cd5d4d1ccfdaf127a1e8d1b8e0584b015f55e40
Choose a base ref
...
head repository: sindresorhus/query-string
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0090a3418253eea4b2c437ba034dd445361325b2
Choose a head ref

Commits on Apr 24, 2020

  1. Add size badge

    sindresorhus authored Apr 24, 2020
    Copy the full SHA
    1e70bf3 View commit details

Commits on Apr 27, 2020

  1. Add benchmarks (#254)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    Vlad Shilov and sindresorhus authored Apr 27, 2020
    Copy the full SHA
    5d19c56 View commit details

Commits on Jun 3, 2020

  1. Meta tweaks

    sindresorhus committed Jun 3, 2020
    Copy the full SHA
    8ee6dbf View commit details

Commits on Jun 5, 2020

  1. Meta tweaks

    sindresorhus committed Jun 5, 2020
    Copy the full SHA
    1ad8bbd View commit details

Commits on Jun 6, 2020

  1. Add support for parsing/stringifying fragment identifier (#222)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    Mark1626 and sindresorhus authored Jun 6, 2020
    Copy the full SHA
    ce06095 View commit details
  2. 6.13.0

    sindresorhus committed Jun 6, 2020
    Copy the full SHA
    d3da28b View commit details

Commits on Jun 11, 2020

  1. Fix sorting existing query params in URL when sort option is false (#…

    …265)
    
    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    dpatwardhan and sindresorhus authored Jun 11, 2020
    Copy the full SHA
    549d677 View commit details
  2. 6.13.1

    sindresorhus committed Jun 11, 2020
    Copy the full SHA
    f38dab5 View commit details

Commits on Sep 13, 2020

  1. Fix the ParsedQuery TypeScript typ

    Fixes #273
    sindresorhus committed Sep 13, 2020
    4
    Copy the full SHA
    56d2923 View commit details
  2. 6.13.2

    sindresorhus committed Sep 13, 2020
    Copy the full SHA
    60802a7 View commit details

Commits on Sep 27, 2020

  1. Copy the full SHA
    b15f945 View commit details
  2. 6.13.3

    sindresorhus committed Sep 27, 2020
    Copy the full SHA
    eb769d2 View commit details

Commits on Sep 28, 2020

  1. Fix the TypeScript types for .stringify() and .stringifyUrl() (#279)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    esetnik and sindresorhus authored Sep 28, 2020
    Copy the full SHA
    38906bc View commit details
  2. 6.13.4

    sindresorhus committed Sep 28, 2020
    Copy the full SHA
    231c160 View commit details

Commits on Oct 2, 2020

  1. Copy the full SHA
    df4cbb3 View commit details
  2. 6.13.5

    sindresorhus committed Oct 2, 2020
    Copy the full SHA
    d40d91d View commit details

Commits on Oct 3, 2020

  1. Copy the full SHA
    7f09a53 View commit details

Commits on Oct 5, 2020

  1. Copy the full SHA
    6427722 View commit details

Commits on Oct 17, 2020

  1. Copy the full SHA
    b38f06c View commit details
  2. 6.13.6

    sindresorhus committed Oct 17, 2020
    Copy the full SHA
    98dabaa View commit details

Commits on Nov 5, 2020

  1. Copy the full SHA
    71d84b7 View commit details
  2. 6.13.7

    sindresorhus committed Nov 5, 2020
    Copy the full SHA
    35846d9 View commit details

Commits on Dec 30, 2020

  1. Work around TypeScript bug

    Fixes #298
    sindresorhus committed Dec 30, 2020
    Copy the full SHA
    94ebcd4 View commit details
  2. 6.13.8

    sindresorhus committed Dec 30, 2020
    Copy the full SHA
    fbe496c View commit details

Commits on Jan 2, 2021

  1. Copy the full SHA
    27453b5 View commit details

Commits on Jan 20, 2021

  1. Update a link

    sindresorhus committed Jan 20, 2021
    Copy the full SHA
    39aef91 View commit details

Commits on Feb 9, 2021

  1. Copy the full SHA
    667c9e9 View commit details

Commits on Feb 10, 2021

  1. Add .pick() and .exclude() (#282)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    Richienb and sindresorhus authored Feb 10, 2021
    Copy the full SHA
    6ed5cb3 View commit details
  2. 6.14.0

    sindresorhus committed Feb 10, 2021
    Copy the full SHA
    2a17881 View commit details

Commits on Feb 26, 2021

  1. Copy the full SHA
    a6d4a3f View commit details
  2. 6.14.1

    sindresorhus committed Feb 26, 2021
    Copy the full SHA
    0090a34 View commit details
Showing with 518 additions and 44 deletions.
  1. +24 −0 .github/workflows/main.yml
  2. +0 −6 .travis.yml
  3. +72 −0 benchmark.js
  4. +146 −4 index.d.ts
  5. +63 −18 index.js
  6. +40 −0 index.test-d.ts
  7. +1 −1 license
  8. +8 −4 package.json
  9. +99 −10 readme.md
  10. +17 −0 test/exclude.js
  11. +7 −0 test/parse-url.js
  12. +11 −1 test/parse.js
  13. +17 −0 test/pick.js
  14. +13 −0 test/stringify-url.js
24 changes: 24 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI
on:
- push
- pull_request
jobs:
test:
name: Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version:
- 14
- 12
- 10
- 8
- 6
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
6 changes: 0 additions & 6 deletions .travis.yml

This file was deleted.

72 changes: 72 additions & 0 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';
const Benchmark = require('benchmark');
const queryString = require('.');

const {stringify, stringifyUrl} = queryString;
const suite = new Benchmark.Suite();

// Fixtures
const TEST_OBJECT = {
genre: 'Epic fantasy',
author: '',
page: 2,
published: true,
symbols: 'πµ',
chapters: [1, 2, 3],
none: null
};
const TEST_HOST = 'https://foo.bar/';
const TEST_STRING = stringify(TEST_OBJECT);
const TEST_BRACKETS_STRING = stringify(TEST_OBJECT, {arrayFormat: 'bracket'});
const TEST_INDEX_STRING = stringify(TEST_OBJECT, {arrayFormat: 'index'});
const TEST_COMMA_STRING = stringify(TEST_OBJECT, {arrayFormat: 'comma'});
const TEST_URL = stringifyUrl({url: TEST_HOST, query: TEST_OBJECT});

// Creates a test case and adds it to the suite
const defineTestCase = (methodName, input, options) => {
const fn = queryString[methodName];
const label = options ? ` (${stringify(options)})` : '';

suite.add(methodName + label, () => fn(input, options || {}));
};

// Define all test cases

// Parse
defineTestCase('parse', TEST_STRING);
defineTestCase('parse', TEST_STRING, {parseNumbers: true});
defineTestCase('parse', TEST_STRING, {parseBooleans: true});
defineTestCase('parse', TEST_STRING, {sort: false});
defineTestCase('parse', TEST_STRING, {decode: false});
defineTestCase('parse', TEST_BRACKETS_STRING, {arrayFormat: 'bracket'});
defineTestCase('parse', TEST_INDEX_STRING, {arrayFormat: 'index'});
defineTestCase('parse', TEST_COMMA_STRING, {arrayFormat: 'comma'});

// Stringify
defineTestCase('stringify', TEST_OBJECT);
defineTestCase('stringify', TEST_OBJECT, {strict: false});
defineTestCase('stringify', TEST_OBJECT, {encode: false});
defineTestCase('stringify', TEST_OBJECT, {skipNull: true});
defineTestCase('stringify', TEST_OBJECT, {skipEmptyString: true});
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'bracket'});
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'index'});
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'comma'});

// Extract
defineTestCase('extract', TEST_URL);

// ParseUrl
defineTestCase('parseUrl', TEST_URL);

// StringifyUrl
defineTestCase('stringifyUrl', {url: TEST_HOST, query: TEST_OBJECT});

// Log/display the results
suite.on('cycle', event => {
const {name, hz} = event.target;
const opsPerSec = Math.round(hz).toLocaleString();

console.log(name.padEnd(36, '_') + opsPerSec.padStart(12, '_') + ' ops/s');
});

suite.run();
150 changes: 146 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -121,10 +121,25 @@ 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> {
[key: string]: T | T[] | null | undefined;
[key: string]: T | T[] | null;
}

/**
@@ -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');
@@ -312,11 +339,23 @@ export interface StringifyOptions {
readonly skipEmptyString?: boolean;
}

export type Stringifiable = string | boolean | number | null | undefined;

export type StringifiableRecord = Record<
string,
Stringifiable | readonly Stringifiable[]
>;

/**
Stringify an object into a query string and sort the keys.
*/
export function stringify(
object: {[key: string]: any},
// TODO: Use the below instead when the following TS issues are fixed:
// - https://github.com/microsoft/TypeScript/issues/15300
// - https://github.com/microsoft/TypeScript/issues/42021
// Context: https://github.com/sindresorhus/query-string/issues/298
// object: StringifiableRecord,
object: Record<string, any>,
options?: StringifyOptions
): string;

@@ -327,21 +366,124 @@ Note: This behaviour can be changed with the `skipNull` option.
*/
export function extract(url: string): string;

export interface UrlObject {
readonly url: string;

/**
Overrides queries in the `url` property.
*/
readonly query?: StringifiableRecord;

/**
Overrides the fragment identifier in the `url` property.
*/
readonly fragmentIdentifier?: string;
}

/**
Stringify an object into a URL with a query string and sorting the keys. The inverse of [`.parseUrl()`](https://github.com/sindresorhus/query-string#parseurlstring-options)
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(
object: ParsedUrl,
object: UrlObject,
options?: StringifyOptions
): string;

/**
Pick query parameters from a URL.
@param url - The URL containing the query parameters to pick.
@param keys - The names of the query parameters to keep. All other query parameters will be removed from the URL.
@param filter - A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`.
@returns The URL with the picked query parameters.
@example
```
queryString.pick('https://foo.bar?foo=1&bar=2#hello', ['foo']);
//=> 'https://foo.bar?foo=1#hello'
queryString.pick('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true});
//=> 'https://foo.bar?bar=2#hello'
```
*/
export function pick(
url: string,
keys: readonly string[],
options?: ParseOptions & StringifyOptions
): string
export function pick(
url: string,
filter: (key: string, value: string | boolean | number) => boolean,
options?: {parseBooleans: true, parseNumbers: true} & ParseOptions & StringifyOptions
): string
export function pick(
url: string,
filter: (key: string, value: string | boolean) => boolean,
options?: {parseBooleans: true} & ParseOptions & StringifyOptions
): string
export function pick(
url: string,
filter: (key: string, value: string | number) => boolean,
options?: {parseNumbers: true} & ParseOptions & StringifyOptions
): string

/**
Exclude query parameters from a URL. Like `.pick()` but reversed.
@param url - The URL containing the query parameters to exclude.
@param keys - The names of the query parameters to remove. All other query parameters will remain in the URL.
@param filter - A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`.
@returns The URL without the excluded the query parameters.
@example
```
queryString.exclude('https://foo.bar?foo=1&bar=2#hello', ['foo']);
//=> 'https://foo.bar?bar=2#hello'
queryString.exclude('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true});
//=> 'https://foo.bar?foo=1#hello'
```
*/
export function exclude(
url: string,
keys: readonly string[],
options?: ParseOptions & StringifyOptions
): string
export function exclude(
url: string,
filter: (key: string, value: string | boolean | number) => boolean,
options?: {parseBooleans: true, parseNumbers: true} & ParseOptions & StringifyOptions
): string
export function exclude(
url: string,
filter: (key: string, value: string | boolean) => boolean,
options?: {parseBooleans: true} & ParseOptions & StringifyOptions
): string
export function exclude(
url: string,
filter: (key: string, value: string | number) => boolean,
options?: {parseNumbers: true} & ParseOptions & StringifyOptions
): string
81 changes: 63 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
const strictUriEncode = require('strict-uri-encode');
const decodeComponent = require('decode-uri-component');
const splitOnFirst = require('split-on-first');
const filterObject = require('filter-obj');

const isNullOrUndefined = value => value === null || value === undefined;

@@ -122,8 +123,10 @@ function parserForArrayFormat(options) {
case 'comma':
case 'separator':
return (key, value, accumulator) => {
const isArray = typeof value === 'string' && value.split('').indexOf(options.arrayFormatSeparator) > -1;
const newValue = isArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : value === null ? value : decode(value, options);
const isArray = typeof value === 'string' && value.includes(options.arrayFormatSeparator);
const isEncodedArray = (typeof value === 'string' && !isArray && decode(value, options).includes(options.arrayFormatSeparator));
value = isEncodedArray ? decode(value, options) : value;
const newValue = isArray || isEncodedArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : value === null ? value : decode(value, options);
accumulator[key] = newValue;
};

@@ -214,7 +217,7 @@ function parseValue(value, options) {
return value;
}

function parse(input, options) {
function parse(query, options) {
options = Object.assign({
decode: true,
sort: true,
@@ -231,17 +234,21 @@ function parse(input, options) {
// Create an object with no prototype
const ret = Object.create(null);

if (typeof input !== 'string') {
if (typeof query !== 'string') {
return ret;
}

input = input.trim().replace(/^[?#&]/, '');
query = query.trim().replace(/^[?#&]/, '');

if (!input) {
if (!query) {
return ret;
}

for (const param of input.split('&')) {
for (const param of query.split('&')) {
if (param === '') {
continue;
}

let [key, value] = splitOnFirst(options.decode ? param.replace(/\+/g, ' ') : param, '=');

// Missing `=` should be `null`:
@@ -337,23 +344,61 @@ exports.stringify = (object, options) => {
}).filter(x => x.length > 0).join('&');
};

exports.parseUrl = (input, options) => {
return {
url: removeHash(input).split('?')[0] || '',
query: parse(extract(input), options)
};
exports.parseUrl = (url, options) => {
options = Object.assign({
decode: true
}, options);

const [url_, hash] = splitOnFirst(url, '#');

return Object.assign(
{
url: url_.split('?')[0] || '',
query: parse(extract(url), options)
},
options && options.parseFragmentIdentifier && hash ? {fragmentIdentifier: decode(hash, options)} : {}
);
};

exports.stringifyUrl = (input, 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);
exports.stringifyUrl = (object, options) => {
options = Object.assign({
encode: true,
strict: true
}, options);

const url = removeHash(object.url).split('?')[0] || '';
const queryFromUrl = exports.extract(object.url);
const parsedQueryFromUrl = exports.parse(queryFromUrl, {sort: false});

const query = Object.assign(parsedQueryFromUrl, object.query);
let queryString = exports.stringify(query, options);
if (queryString) {
queryString = `?${queryString}`;
}

let hash = getHash(object.url);
if (object.fragmentIdentifier) {
hash = `#${encode(object.fragmentIdentifier, options)}`;
}

return `${url}${queryString}${hash}`;
};

exports.pick = (input, filter, options) => {
options = Object.assign({
parseFragmentIdentifier: true
}, options);

const {url, query, fragmentIdentifier} = exports.parseUrl(input, options);
return exports.stringifyUrl({
url,
query: filterObject(query, filter),
fragmentIdentifier
}, options);
};

exports.exclude = (input, filter, options) => {
const exclusionFilter = Array.isArray(filter) ? key => !filter.includes(key) : (key, value) => !filter(key, value);

return exports.pick(input, exclusionFilter, options);
};
40 changes: 40 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -35,6 +35,17 @@ expectType<string>(
)
);

// Ensure it accepts an `interface`.
interface Query {
foo: string;
}

const query: Query = {
foo: 'bar'
};

queryString.stringify(query);

// Parse
expectType<queryString.ParsedQuery>(queryString.parse('?foo=bar'));

@@ -87,6 +98,35 @@ 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'));

expectType<string>(
queryString.stringifyUrl({
url: 'https://sindresorhus.com',
query: {
fooMixedArray: [
'a',
1,
true,
null,
undefined
],
fooNumber: 1,
fooBoolean: true,
fooNull: null,
fooUndefined: undefined,
fooString: 'hi'
},
})
);

// Pick
expectType<string>(queryString.pick('http://foo.bar/?abc=def&hij=klm', ['abc']))

// Exclude
expectType<string>(queryString.exclude('http://foo.bar/?abc=def&hij=klm', ['abc']))
2 changes: 1 addition & 1 deletion license
Original file line number Diff line number Diff line change
@@ -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:

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
{
"name": "query-string",
"version": "6.12.1",
"version": "6.14.1",
"description": "Parse and stringify URL query strings",
"license": "MIT",
"repository": "sindresorhus/query-string",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
"url": "https://sindresorhus.com"
},
"engines": {
"node": ">=6"
},
"scripts": {
"benchmark": "node benchmark.js",
"test": "xo && ava && tsd"
},
"files": [
@@ -33,15 +34,18 @@
"stringify",
"encode",
"decode",
"searchparams"
"searchparams",
"filter"
],
"dependencies": {
"split-on-first": "^1.0.0",
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"devDependencies": {
"ava": "^1.4.1",
"benchmark": "^2.1.4",
"deep-equal": "^1.0.1",
"fast-check": "^1.5.0",
"tsd": "^0.7.3",
109 changes: 99 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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)
# 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/tech">
<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
@@ -384,6 +415,64 @@ Type: `object`

Query items to add to the URL.

### .pick(url, keys, options?)
### .pick(url, filter, options?)

Pick query parameters from a URL.

Returns a string with the new URL.

```js
const queryString = require('query-string');

queryString.pick('https://foo.bar?foo=1&bar=2#hello', ['foo']);
//=> 'https://foo.bar?foo=1#hello'

queryString.pick('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true});
//=> 'https://foo.bar?bar=2#hello'
```

### .exclude(url, keys, options?)
### .exclude(url, filter, options?)

Exclude query parameters from a URL.

Returns a string with the new URL.

```js
const queryString = require('query-string');

queryString.exclude('https://foo.bar?foo=1&bar=2#hello', ['foo']);
//=> 'https://foo.bar?bar=2#hello'

queryString.exclude('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true});
//=> 'https://foo.bar?foo=1#hello'
```

#### url

Type: `string`

The URL containing the query parameters to filter.

#### keys

Type: `string[]`

The names of the query parameters to filter based on the function used.

#### filter

Type: `(key, value) => boolean`

A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`.

#### options

Type: `object`

[Parse options](#options) and [stringify options](#options-1).

## Nesting

This module intentionally doesn't support nesting as it's not spec'd and varies between implementations, which causes a lot of [edge cases](https://github.com/visionmedia/node-querystring/issues).
17 changes: 17 additions & 0 deletions test/exclude.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import test from 'ava';
import queryString from '..';

test('excludes elements in a URL with a filter array', t => {
t.is(queryString.exclude('http://example.com/?a=1&b=2&c=3#a', ['c']), 'http://example.com/?a=1&b=2#a');
});

test('excludes elements in a URL with a filter predicate', t => {
t.is(queryString.exclude('http://example.com/?a=1&b=2&c=3#a', (name, value) => {
t.is(typeof name, 'string');
t.is(typeof value, 'number');

return name === 'a';
}, {
parseNumbers: true
}), 'http://example.com/?b=2&c=3#a');
});
7 changes: 7 additions & 0 deletions test/parse-url.js
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 11 additions & 1 deletion test/parse.js
Original file line number Diff line number Diff line change
@@ -13,6 +13,11 @@ test('query strings starting with a `&`', t => {
t.deepEqual(queryString.parse('&foo=bar&foo=baz'), {foo: ['bar', 'baz']});
});

test('query strings ending with a `&`', t => {
t.deepEqual(queryString.parse('foo=bar&'), {foo: 'bar'});
t.deepEqual(queryString.parse('foo=bar&&&'), {foo: 'bar'});
});

test('parse a query string', t => {
t.deepEqual(queryString.parse('foo=bar'), {foo: 'bar'});
});
@@ -75,6 +80,11 @@ test('handle `+` correctly', t => {
t.deepEqual(queryString.parse('foo+faz=bar+baz++'), {'foo faz': 'bar baz '});
});

test('parses numbers with exponential notation as string', t => {
t.deepEqual(queryString.parse('192e11=bar'), {'192e11': 'bar'});
t.deepEqual(queryString.parse('bar=192e11'), {bar: '192e11'});
});

test('handle `+` correctly when not decoding', t => {
t.deepEqual(queryString.parse('foo+faz=bar+baz++', {decode: false}), {'foo+faz': 'bar+baz++'});
});
@@ -318,7 +328,7 @@ test('value should not be decoded twice with `arrayFormat` option set as `separa
});

// See https://github.com/sindresorhus/query-string/issues/242
test.failing('value separated by encoded comma will not be parsed as array with `arrayFormat` option set to `comma`', t => {
test('value separated by encoded comma will not be parsed as array with `arrayFormat` option set to `comma`', t => {
t.deepEqual(queryString.parse('id=1%2C2%2C3', {arrayFormat: 'comma', parseNumbers: true}), {
id: [1, 2, 3]
});
17 changes: 17 additions & 0 deletions test/pick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import test from 'ava';
import queryString from '..';

test('picks elements in a URL with a filter array', t => {
t.is(queryString.pick('http://example.com/?a=1&b=2&c=3#a', ['a', 'b']), 'http://example.com/?a=1&b=2#a');
});

test('picks elements in a URL with a filter predicate', t => {
t.is(queryString.pick('http://example.com/?a=1&b=2&c=3#a', (name, value) => {
t.is(typeof name, 'string');
t.is(typeof value, 'number');

return name === 'a';
}, {
parseNumbers: true
}), 'http://example.com/?a=1#a');
});
13 changes: 13 additions & 0 deletions test/stringify-url.js
Original file line number Diff line number Diff line change
@@ -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};

@@ -43,3 +52,7 @@ test('stringify URL from the result of `parseUrl` with query string that contain
const parsedUrl = queryString.parseUrl(url);
t.deepEqual(queryString.stringifyUrl(parsedUrl, {encode: false}), url);
});

test('stringify URL without sorting existing query params', t => {
t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?C=3&A=1', query: {D: 4, B: 2}}, {sort: false}), 'https://foo.bar?C=3&A=1&D=4&B=2');
});