• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2const strictUriEncode = require('strict-uri-encode');
3const decodeComponent = require('decode-uri-component');
4const splitOnFirst = require('split-on-first');
5
6function encoderForArrayFormat(options) {
7	switch (options.arrayFormat) {
8		case 'index':
9			return key => (result, value) => {
10				const index = result.length;
11				if (value === undefined) {
12					return result;
13				}
14
15				if (value === null) {
16					return [...result, [encode(key, options), '[', index, ']'].join('')];
17				}
18
19				return [
20					...result,
21					[encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join('')
22				];
23			};
24
25		case 'bracket':
26			return key => (result, value) => {
27				if (value === undefined) {
28					return result;
29				}
30
31				if (value === null) {
32					return [...result, [encode(key, options), '[]'].join('')];
33				}
34
35				return [...result, [encode(key, options), '[]=', encode(value, options)].join('')];
36			};
37
38		case 'comma':
39			return key => (result, value, index) => {
40				if (value === null || value === undefined || value.length === 0) {
41					return result;
42				}
43
44				if (index === 0) {
45					return [[encode(key, options), '=', encode(value, options)].join('')];
46				}
47
48				return [[result, encode(value, options)].join(',')];
49			};
50
51		default:
52			return key => (result, value) => {
53				if (value === undefined) {
54					return result;
55				}
56
57				if (value === null) {
58					return [...result, encode(key, options)];
59				}
60
61				return [...result, [encode(key, options), '=', encode(value, options)].join('')];
62			};
63	}
64}
65
66function parserForArrayFormat(options) {
67	let result;
68
69	switch (options.arrayFormat) {
70		case 'index':
71			return (key, value, accumulator) => {
72				result = /\[(\d*)\]$/.exec(key);
73
74				key = key.replace(/\[\d*\]$/, '');
75
76				if (!result) {
77					accumulator[key] = value;
78					return;
79				}
80
81				if (accumulator[key] === undefined) {
82					accumulator[key] = {};
83				}
84
85				accumulator[key][result[1]] = value;
86			};
87
88		case 'bracket':
89			return (key, value, accumulator) => {
90				result = /(\[\])$/.exec(key);
91				key = key.replace(/\[\]$/, '');
92
93				if (!result) {
94					accumulator[key] = value;
95					return;
96				}
97
98				if (accumulator[key] === undefined) {
99					accumulator[key] = [value];
100					return;
101				}
102
103				accumulator[key] = [].concat(accumulator[key], value);
104			};
105
106		case 'comma':
107			return (key, value, accumulator) => {
108				const isArray = typeof value === 'string' && value.split('').indexOf(',') > -1;
109				const newValue = isArray ? value.split(',') : value;
110				accumulator[key] = newValue;
111			};
112
113		default:
114			return (key, value, accumulator) => {
115				if (accumulator[key] === undefined) {
116					accumulator[key] = value;
117					return;
118				}
119
120				accumulator[key] = [].concat(accumulator[key], value);
121			};
122	}
123}
124
125function encode(value, options) {
126	if (options.encode) {
127		return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
128	}
129
130	return value;
131}
132
133function decode(value, options) {
134	if (options.decode) {
135		return decodeComponent(value);
136	}
137
138	return value;
139}
140
141function keysSorter(input) {
142	if (Array.isArray(input)) {
143		return input.sort();
144	}
145
146	if (typeof input === 'object') {
147		return keysSorter(Object.keys(input))
148			.sort((a, b) => Number(a) - Number(b))
149			.map(key => input[key]);
150	}
151
152	return input;
153}
154
155function removeHash(input) {
156	const hashStart = input.indexOf('#');
157	if (hashStart !== -1) {
158		input = input.slice(0, hashStart);
159	}
160
161	return input;
162}
163
164function extract(input) {
165	input = removeHash(input);
166	const queryStart = input.indexOf('?');
167	if (queryStart === -1) {
168		return '';
169	}
170
171	return input.slice(queryStart + 1);
172}
173
174function parse(input, options) {
175	options = Object.assign({
176		decode: true,
177		sort: true,
178		arrayFormat: 'none',
179		parseNumbers: false,
180		parseBooleans: false
181	}, options);
182
183	const formatter = parserForArrayFormat(options);
184
185	// Create an object with no prototype
186	const ret = Object.create(null);
187
188	if (typeof input !== 'string') {
189		return ret;
190	}
191
192	input = input.trim().replace(/^[?#&]/, '');
193
194	if (!input) {
195		return ret;
196	}
197
198	for (const param of input.split('&')) {
199		let [key, value] = splitOnFirst(param.replace(/\+/g, ' '), '=');
200
201		// Missing `=` should be `null`:
202		// http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
203		value = value === undefined ? null : decode(value, options);
204
205		if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
206			value = Number(value);
207		} else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
208			value = value.toLowerCase() === 'true';
209		}
210
211		formatter(decode(key, options), value, ret);
212	}
213
214	if (options.sort === false) {
215		return ret;
216	}
217
218	return (options.sort === true ? Object.keys(ret).sort() : Object.keys(ret).sort(options.sort)).reduce((result, key) => {
219		const value = ret[key];
220		if (Boolean(value) && typeof value === 'object' && !Array.isArray(value)) {
221			// Sort object keys, not values
222			result[key] = keysSorter(value);
223		} else {
224			result[key] = value;
225		}
226
227		return result;
228	}, Object.create(null));
229}
230
231exports.extract = extract;
232exports.parse = parse;
233
234exports.stringify = (object, options) => {
235	if (!object) {
236		return '';
237	}
238
239	options = Object.assign({
240		encode: true,
241		strict: true,
242		arrayFormat: 'none'
243	}, options);
244
245	const formatter = encoderForArrayFormat(options);
246	const keys = Object.keys(object);
247
248	if (options.sort !== false) {
249		keys.sort(options.sort);
250	}
251
252	return keys.map(key => {
253		const value = object[key];
254
255		if (value === undefined) {
256			return '';
257		}
258
259		if (value === null) {
260			return encode(key, options);
261		}
262
263		if (Array.isArray(value)) {
264			return value
265				.reduce(formatter(key), [])
266				.join('&');
267		}
268
269		return encode(key, options) + '=' + encode(value, options);
270	}).filter(x => x.length > 0).join('&');
271};
272
273exports.parseUrl = (input, options) => {
274	return {
275		url: removeHash(input).split('?')[0] || '',
276		query: parse(extract(input), options)
277	};
278};
279