From ce0609566f64ad6a5bc495ea48ea492265de45bf Mon Sep 17 00:00:00 2001 From: Nimalan Date: Sat, 6 Jun 2020 12:15:47 +0530 Subject: [PATCH] Add support for parsing/stringifying fragment identifier (#222) Co-authored-by: Sindre Sorhus --- index.d.ts | 38 ++++++++++++++++++++++++++++++++++++++ index.js | 29 ++++++++++++++++++++++++----- index.test-d.ts | 3 +++ readme.md | 40 ++++++++++++++++++++++++++++++++++++++-- test/parse-url.js | 7 +++++++ test/stringify-url.js | 9 +++++++++ 6 files changed, 119 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index ccf166dd..b3a3c99a 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 { @@ -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; @@ -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 0b57b3ec..f8ca0df7 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/index.test-d.ts b/index.test-d.ts index 19a6817c..a7563e8a 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -87,6 +87,9 @@ expectType( expectType( queryString.parseUrl('?foo=true', {parseBooleans: true}) ); +expectType( + queryString.parseUrl('?foo=true#bar', {parseFragmentIdentifier: true}) +); // Extract expectType(queryString.extract('http://foo.bar/?abc=def&hij=klm')); diff --git a/readme.md b/readme.md index adb569f4..46e58b5b 100644 --- a/readme.md +++ b/readme.md @@ -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?) @@ -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 diff --git a/test/parse-url.js b/test/parse-url.js index a85e88e9..720333c4 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 933af7a2..98055cc1 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};