├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: 13 | - 14 14 | - 16 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node }} 21 | - run: npm install 22 | - run: npm test 23 | - uses: coverallsapp/github-action@1.1.3 24 | if: matrix.node == 18 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | test.js 3 | .* 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # querystringify 2 | 3 | [![Version npm](https://img.shields.io/npm/v/querystringify.svg?style=flat-square)](https://www.npmjs.com/package/querystringify)[![CI](https://img.shields.io/github/actions/workflow/status/unshiftio/querystringify/ci.yml?branch=master&label=CI&style=flat-square)](https://github.com/unshiftio/querystringify/actions?query=workflow%3ACI+branch%3Amaster)[![Coverage Status](https://img.shields.io/coveralls/unshiftio/querystringify/master.svg?style=flat-square)](https://coveralls.io/r/unshiftio/querystringify?branch=master) 4 | 5 | First off, see if the built-in 6 | [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) 7 | is suitable for your needs. 8 | 9 | Development of this module started in 2014, when `URLSearchParams` wasn't 10 | available. The module provides a somewhat JSON-compatible interface for query 11 | string parsing. This query string parser is dumb, don't expect to much from it 12 | as it only wants to parse simple query strings. If you want to parse complex, 13 | multi level and deeply nested query strings then you should rethink your 14 | approach, due to the lack of spec and numerous edge cases. 15 | 16 | ## Installation 17 | 18 | This module is released in npm as `querystringify`. It's also compatible with 19 | `browserify` so it can be used on the server as well as on the client. To 20 | install it simply run the following command from your CLI: 21 | 22 | ``` 23 | npm install --save querystringify 24 | ``` 25 | 26 | ## Usage 27 | 28 | In the following examples we assume that you've already required the library as: 29 | 30 | ```js 31 | 'use strict'; 32 | 33 | var qs = require('querystringify'); 34 | ``` 35 | 36 | ### qs.parse() 37 | 38 | The parse method transforms a given query string in to an object. Parameters 39 | without values are set to empty strings. It does not care if your query string 40 | is prefixed with a `?`, a `#`, or not prefixed. It just extracts the parts 41 | between the `=` and `&`: 42 | 43 | ```js 44 | qs.parse('?foo=bar'); // { foo: 'bar' } 45 | qs.parse('#foo=bar'); // { foo: 'bar' } 46 | qs.parse('foo=bar'); // { foo: 'bar' } 47 | qs.parse('foo=bar&bar=foo'); // { foo: 'bar', bar: 'foo' } 48 | qs.parse('foo&bar=foo'); // { foo: '', bar: 'foo' } 49 | ``` 50 | 51 | ### qs.stringify() 52 | 53 | This transforms a given object in to a query string. By default we return the 54 | query string without a `?` prefix. If you want to prefix it by default simply 55 | supply `true` as second argument. If it should be prefixed by something else 56 | simply supply a string with the prefix value as second argument: 57 | 58 | ```js 59 | qs.stringify({ foo: bar }); // foo=bar 60 | qs.stringify({ foo: bar }, true); // ?foo=bar 61 | qs.stringify({ foo: bar }, '#'); // #foo=bar 62 | qs.stringify({ foo: '' }, '&'); // &foo= 63 | ``` 64 | 65 | ## License 66 | 67 | MIT 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var has = Object.prototype.hasOwnProperty 4 | , undef; 5 | 6 | /** 7 | * Decode a URI encoded string. 8 | * 9 | * @param {String} input The URI encoded string. 10 | * @returns {String|Null} The decoded string. 11 | * @api private 12 | */ 13 | function decode(input) { 14 | try { 15 | return decodeURIComponent(input.replace(/\+/g, ' ')); 16 | } catch (e) { 17 | return null; 18 | } 19 | } 20 | 21 | /** 22 | * Attempts to encode a given input. 23 | * 24 | * @param {String} input The string that needs to be encoded. 25 | * @returns {String|Null} The encoded string. 26 | * @api private 27 | */ 28 | function encode(input) { 29 | try { 30 | return encodeURIComponent(input); 31 | } catch (e) { 32 | return null; 33 | } 34 | } 35 | 36 | /** 37 | * Simple query string parser. 38 | * 39 | * @param {String} query The query string that needs to be parsed. 40 | * @returns {Object} 41 | * @api public 42 | */ 43 | function querystring(query) { 44 | var parser = /([^=?#&]+)=?([^&]*)/g 45 | , result = {} 46 | , part; 47 | 48 | while (part = parser.exec(query)) { 49 | var key = decode(part[1]) 50 | , value = decode(part[2]); 51 | 52 | // 53 | // Prevent overriding of existing properties. This ensures that build-in 54 | // methods like `toString` or __proto__ are not overriden by malicious 55 | // querystrings. 56 | // 57 | // In the case if failed decoding, we want to omit the key/value pairs 58 | // from the result. 59 | // 60 | if (key === null || value === null || key in result) continue; 61 | result[key] = value; 62 | } 63 | 64 | return result; 65 | } 66 | 67 | /** 68 | * Transform a query string to an object. 69 | * 70 | * @param {Object} obj Object that should be transformed. 71 | * @param {String} prefix Optional prefix. 72 | * @returns {String} 73 | * @api public 74 | */ 75 | function querystringify(obj, prefix) { 76 | prefix = prefix || ''; 77 | 78 | var pairs = [] 79 | , value 80 | , key; 81 | 82 | // 83 | // Optionally prefix with a '?' if needed 84 | // 85 | if ('string' !== typeof prefix) prefix = '?'; 86 | 87 | for (key in obj) { 88 | if (has.call(obj, key)) { 89 | value = obj[key]; 90 | 91 | // 92 | // Edge cases where we actually want to encode the value to an empty 93 | // string instead of the stringified value. 94 | // 95 | if (!value && (value === null || value === undef || isNaN(value))) { 96 | value = ''; 97 | } 98 | 99 | key = encode(key); 100 | value = encode(value); 101 | 102 | // 103 | // If we failed to encode the strings, we should bail out as we don't 104 | // want to add invalid strings to the query. 105 | // 106 | if (key === null || value === null) continue; 107 | pairs.push(key +'='+ value); 108 | } 109 | } 110 | 111 | return pairs.length ? prefix + pairs.join('&') : ''; 112 | } 113 | 114 | // 115 | // Expose the module. 116 | // 117 | exports.stringify = querystringify; 118 | exports.parse = querystring; 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "querystringify", 3 | "version": "2.2.0", 4 | "description": "Querystringify - Small, simple but powerful query string parser.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "c8 --reporter=lcov --reporter=text mocha test.js", 8 | "watch": "mocha --watch test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/unshiftio/querystringify" 13 | }, 14 | "keywords": [ 15 | "query", 16 | "string", 17 | "query-string", 18 | "querystring", 19 | "qs", 20 | "stringify", 21 | "parse", 22 | "decode", 23 | "encode" 24 | ], 25 | "author": "Arnout Kazemier", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/unshiftio/querystringify/issues" 29 | }, 30 | "homepage": "https://github.com/unshiftio/querystringify", 31 | "devDependencies": { 32 | "assume": "^2.1.0", 33 | "c8": "^7.3.0", 34 | "mocha": "^10.2.0", 35 | "pre-commit": "^1.2.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | describe('querystringify', function () { 2 | 'use strict'; 3 | 4 | var assume = require('assume') 5 | , qs = require('./'); 6 | 7 | describe('#stringify', function () { 8 | var obj = { 9 | foo: 'bar', 10 | bar: 'foo' 11 | }; 12 | 13 | it('is exposed as method', function () { 14 | assume(qs.stringify).is.a('function'); 15 | }); 16 | 17 | it('transforms an object', function () { 18 | assume(qs.stringify(obj)).equals('foo=bar&bar=foo'); 19 | }); 20 | 21 | it('can optionally prefix', function () { 22 | assume(qs.stringify(obj, true)).equals('?foo=bar&bar=foo'); 23 | }); 24 | 25 | it('can prefix with custom things', function () { 26 | assume(qs.stringify(obj, '&')).equals('&foo=bar&bar=foo'); 27 | }); 28 | 29 | it('doesnt prefix empty query strings', function () { 30 | assume(qs.stringify({}, true)).equals(''); 31 | assume(qs.stringify({})).equals(''); 32 | }); 33 | 34 | it('works with object keys with empty string values', function () { 35 | assume(qs.stringify({ foo: '' })).equals('foo='); 36 | }); 37 | 38 | it('works with nulled objects', function () { 39 | var obj = Object.create(null); 40 | 41 | obj.foo='bar'; 42 | assume(qs.stringify(obj)).equals('foo=bar'); 43 | }); 44 | 45 | it('transforms `undefined` into nothing', function () { 46 | assume(qs.stringify({ foo: undefined })).equals('foo='); 47 | }); 48 | 49 | it('transforms `NaN` into nothing', function () { 50 | assume(qs.stringify({ foo: NaN })).equals('foo='); 51 | }); 52 | 53 | it('transforms `null` into nothing', function () { 54 | assume(qs.stringify({ foo: null })).equals('foo='); 55 | }); 56 | 57 | if (typeof Symbol !== 'undefined') { 58 | it('does not throw on invalid input', function () { 59 | assume(qs.stringify({ foo: Symbol('foo') })).equals(''); 60 | }); 61 | } 62 | }); 63 | 64 | describe('#parse', function () { 65 | it('is exposed as method', function () { 66 | assume(qs.parse).is.a('function'); 67 | }); 68 | 69 | it('will parse a querystring to an object', function () { 70 | var obj = qs.parse('foo=bar'); 71 | 72 | assume(obj).is.a('object'); 73 | assume(obj.foo).equals('bar'); 74 | }); 75 | 76 | it('will also work if querystring is prefixed with ?', function () { 77 | var obj = qs.parse('?foo=bar&shizzle=mynizzle'); 78 | 79 | assume(obj).is.a('object'); 80 | assume(obj.foo).equals('bar'); 81 | assume(obj.shizzle).equals('mynizzle'); 82 | }); 83 | 84 | it('will also work if querystring is prefixed with #', function () { 85 | var obj = qs.parse('#foo=bar&shizzle=mynizzle'); 86 | 87 | assume(obj).is.a('object'); 88 | assume(obj.foo).equals('bar'); 89 | assume(obj.shizzle).equals('mynizzle'); 90 | }); 91 | 92 | it('does not overide prototypes', function () { 93 | var obj = qs.parse('?toString&__proto__=lol'); 94 | 95 | assume(obj).is.a('object'); 96 | assume(obj.toString).is.a('function'); 97 | assume(obj.__proto__).does.not.equals('lol'); 98 | }); 99 | 100 | it('works with querystring parameters without values', function () { 101 | var obj = qs.parse('?foo&bar=&shizzle=mynizzle'); 102 | 103 | assume(obj).is.a('object'); 104 | assume(obj.foo).equals(''); 105 | assume(obj.bar).equals(''); 106 | assume(obj.shizzle).equals('mynizzle'); 107 | }); 108 | 109 | it('first value wins', function () { 110 | var obj = qs.parse('foo=1&foo=2'); 111 | 112 | assume(obj.foo).equals('1'); 113 | }); 114 | 115 | it('decodes plus signs', function () { 116 | var obj = qs.parse('foo+bar=baz+qux'); 117 | 118 | assume(obj).is.a('object'); 119 | assume(obj['foo bar']).equals('baz qux'); 120 | 121 | obj = qs.parse('foo+bar=baz%2Bqux'); 122 | 123 | assume(obj).is.a('object'); 124 | assume(obj['foo bar']).equals('baz+qux'); 125 | }); 126 | 127 | it('does not throw on invalid input', function () { 128 | var obj = qs.parse('?%&'); 129 | 130 | assume(obj).is.a('object'); 131 | }); 132 | 133 | it('does not include invalid output', function () { 134 | var obj = qs.parse('?%&'); 135 | 136 | assume(obj).is.a('object'); 137 | assume(obj).is.length(0); 138 | }); 139 | }); 140 | }); 141 | --------------------------------------------------------------------------------