query-string 6.13.8 → 9.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. package/base.d.ts +775 -0
  2. package/base.js +578 -0
  3. package/index.d.ts +16 -411
  4. package/index.js +2 -379
  5. package/license +1 -1
  6. package/package.json +26 -12
  7. package/readme.md +366 -56
package/base.js ADDED
@@ -0,0 +1,578 @@
1
+ import decodeComponent from 'decode-uri-component';
2
+ import {includeKeys} from 'filter-obj';
3
+ import splitOnFirst from 'split-on-first';
4
+
5
+ const isNullOrUndefined = value => value === null || value === undefined;
6
+
7
+ // eslint-disable-next-line unicorn/prefer-code-point
8
+ const strictUriEncode = string => encodeURIComponent(string).replaceAll(/[!'()*]/g, x => `%${x.charCodeAt(0).toString(16).toUpperCase()}`);
9
+
10
+ const encodeFragmentIdentifier = Symbol('encodeFragmentIdentifier');
11
+
12
+ function encoderForArrayFormat(options) {
13
+ switch (options.arrayFormat) {
14
+ case 'index': {
15
+ return key => (result, value) => {
16
+ const index = result.length;
17
+
18
+ if (
19
+ value === undefined
20
+ || (options.skipNull && value === null)
21
+ || (options.skipEmptyString && value === '')
22
+ ) {
23
+ return result;
24
+ }
25
+
26
+ if (value === null) {
27
+ return [
28
+ ...result, [encode(key, options), '[', index, ']'].join(''),
29
+ ];
30
+ }
31
+
32
+ return [
33
+ ...result,
34
+ [encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join(''),
35
+ ];
36
+ };
37
+ }
38
+
39
+ case 'bracket': {
40
+ return key => (result, value) => {
41
+ if (
42
+ value === undefined
43
+ || (options.skipNull && value === null)
44
+ || (options.skipEmptyString && value === '')
45
+ ) {
46
+ return result;
47
+ }
48
+
49
+ if (value === null) {
50
+ return [
51
+ ...result,
52
+ [encode(key, options), '[]'].join(''),
53
+ ];
54
+ }
55
+
56
+ return [
57
+ ...result,
58
+ [encode(key, options), '[]=', encode(value, options)].join(''),
59
+ ];
60
+ };
61
+ }
62
+
63
+ case 'colon-list-separator': {
64
+ return key => (result, value) => {
65
+ if (
66
+ value === undefined
67
+ || (options.skipNull && value === null)
68
+ || (options.skipEmptyString && value === '')
69
+ ) {
70
+ return result;
71
+ }
72
+
73
+ if (value === null) {
74
+ return [
75
+ ...result,
76
+ [encode(key, options), ':list='].join(''),
77
+ ];
78
+ }
79
+
80
+ return [
81
+ ...result,
82
+ [encode(key, options), ':list=', encode(value, options)].join(''),
83
+ ];
84
+ };
85
+ }
86
+
87
+ case 'comma':
88
+ case 'separator':
89
+ case 'bracket-separator': {
90
+ const keyValueSeparator = options.arrayFormat === 'bracket-separator'
91
+ ? '[]='
92
+ : '=';
93
+
94
+ return key => (result, value) => {
95
+ if (
96
+ value === undefined
97
+ || (options.skipNull && value === null)
98
+ || (options.skipEmptyString && value === '')
99
+ ) {
100
+ return result;
101
+ }
102
+
103
+ // Translate null to an empty string so that it doesn't serialize as 'null'
104
+ value = value === null ? '' : value;
105
+
106
+ if (result.length === 0) {
107
+ return [[encode(key, options), keyValueSeparator, encode(value, options)].join('')];
108
+ }
109
+
110
+ return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
111
+ };
112
+ }
113
+
114
+ default: {
115
+ return key => (result, value) => {
116
+ if (
117
+ value === undefined
118
+ || (options.skipNull && value === null)
119
+ || (options.skipEmptyString && value === '')
120
+ ) {
121
+ return result;
122
+ }
123
+
124
+ if (value === null) {
125
+ return [
126
+ ...result,
127
+ encode(key, options),
128
+ ];
129
+ }
130
+
131
+ return [
132
+ ...result,
133
+ [encode(key, options), '=', encode(value, options)].join(''),
134
+ ];
135
+ };
136
+ }
137
+ }
138
+ }
139
+
140
+ function parserForArrayFormat(options) {
141
+ let result;
142
+
143
+ switch (options.arrayFormat) {
144
+ case 'index': {
145
+ return (key, value, accumulator) => {
146
+ result = /\[(\d*)]$/.exec(key);
147
+
148
+ key = key.replace(/\[\d*]$/, '');
149
+
150
+ if (!result) {
151
+ accumulator[key] = value;
152
+ return;
153
+ }
154
+
155
+ if (accumulator[key] === undefined) {
156
+ accumulator[key] = {};
157
+ }
158
+
159
+ accumulator[key][result[1]] = value;
160
+ };
161
+ }
162
+
163
+ case 'bracket': {
164
+ return (key, value, accumulator) => {
165
+ result = /(\[])$/.exec(key);
166
+ key = key.replace(/\[]$/, '');
167
+
168
+ if (!result) {
169
+ accumulator[key] = value;
170
+ return;
171
+ }
172
+
173
+ if (accumulator[key] === undefined) {
174
+ accumulator[key] = [value];
175
+ return;
176
+ }
177
+
178
+ accumulator[key] = [...accumulator[key], value];
179
+ };
180
+ }
181
+
182
+ case 'colon-list-separator': {
183
+ return (key, value, accumulator) => {
184
+ result = /(:list)$/.exec(key);
185
+ key = key.replace(/:list$/, '');
186
+
187
+ if (!result) {
188
+ accumulator[key] = value;
189
+ return;
190
+ }
191
+
192
+ if (accumulator[key] === undefined) {
193
+ accumulator[key] = [value];
194
+ return;
195
+ }
196
+
197
+ accumulator[key] = [...accumulator[key], value];
198
+ };
199
+ }
200
+
201
+ case 'comma':
202
+ case 'separator': {
203
+ return (key, value, accumulator) => {
204
+ const isArray = typeof value === 'string' && value.includes(options.arrayFormatSeparator);
205
+ const newValue = isArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : (value === null ? value : decode(value, options));
206
+ accumulator[key] = newValue;
207
+ };
208
+ }
209
+
210
+ case 'bracket-separator': {
211
+ return (key, value, accumulator) => {
212
+ const isArray = /(\[])$/.test(key);
213
+ key = key.replace(/\[]$/, '');
214
+
215
+ if (!isArray) {
216
+ accumulator[key] = value ? decode(value, options) : value;
217
+ return;
218
+ }
219
+
220
+ const arrayValue = value === null
221
+ ? []
222
+ : decode(value, options).split(options.arrayFormatSeparator);
223
+
224
+ if (accumulator[key] === undefined) {
225
+ accumulator[key] = arrayValue;
226
+ return;
227
+ }
228
+
229
+ accumulator[key] = [...accumulator[key], ...arrayValue];
230
+ };
231
+ }
232
+
233
+ default: {
234
+ return (key, value, accumulator) => {
235
+ if (accumulator[key] === undefined) {
236
+ accumulator[key] = value;
237
+ return;
238
+ }
239
+
240
+ accumulator[key] = [...[accumulator[key]].flat(), value];
241
+ };
242
+ }
243
+ }
244
+ }
245
+
246
+ function validateArrayFormatSeparator(value) {
247
+ if (typeof value !== 'string' || value.length !== 1) {
248
+ throw new TypeError('arrayFormatSeparator must be single character string');
249
+ }
250
+ }
251
+
252
+ function encode(value, options) {
253
+ if (options.encode) {
254
+ return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
255
+ }
256
+
257
+ return value;
258
+ }
259
+
260
+ function decode(value, options) {
261
+ if (options.decode) {
262
+ return decodeComponent(value);
263
+ }
264
+
265
+ return value;
266
+ }
267
+
268
+ function keysSorter(input) {
269
+ if (Array.isArray(input)) {
270
+ return input.sort();
271
+ }
272
+
273
+ if (typeof input === 'object') {
274
+ return keysSorter(Object.keys(input))
275
+ .sort((a, b) => Number(a) - Number(b))
276
+ .map(key => input[key]);
277
+ }
278
+
279
+ return input;
280
+ }
281
+
282
+ function removeHash(input) {
283
+ const hashStart = input.indexOf('#');
284
+ if (hashStart !== -1) {
285
+ input = input.slice(0, hashStart);
286
+ }
287
+
288
+ return input;
289
+ }
290
+
291
+ function getHash(url) {
292
+ let hash = '';
293
+ const hashStart = url.indexOf('#');
294
+ if (hashStart !== -1) {
295
+ hash = url.slice(hashStart);
296
+ }
297
+
298
+ return hash;
299
+ }
300
+
301
+ function parseValue(value, options, type) {
302
+ if (type === 'string' && typeof value === 'string') {
303
+ return value;
304
+ }
305
+
306
+ if (typeof type === 'function' && typeof value === 'string') {
307
+ return type(value);
308
+ }
309
+
310
+ if (type === 'boolean' && value === null) {
311
+ return true;
312
+ }
313
+
314
+ if (type === 'boolean' && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
315
+ return value.toLowerCase() === 'true';
316
+ }
317
+
318
+ if (type === 'boolean' && value !== null && (value.toLowerCase() === '1' || value.toLowerCase() === '0')) {
319
+ return value.toLowerCase() === '1';
320
+ }
321
+
322
+ if (type === 'string[]' && options.arrayFormat !== 'none' && typeof value === 'string') {
323
+ return [value];
324
+ }
325
+
326
+ if (type === 'number[]' && options.arrayFormat !== 'none' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
327
+ return [Number(value)];
328
+ }
329
+
330
+ if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
331
+ return Number(value);
332
+ }
333
+
334
+ if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
335
+ return value.toLowerCase() === 'true';
336
+ }
337
+
338
+ if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
339
+ return Number(value);
340
+ }
341
+
342
+ return value;
343
+ }
344
+
345
+ export function extract(input) {
346
+ input = removeHash(input);
347
+ const queryStart = input.indexOf('?');
348
+ if (queryStart === -1) {
349
+ return '';
350
+ }
351
+
352
+ return input.slice(queryStart + 1);
353
+ }
354
+
355
+ export function parse(query, options) {
356
+ options = {
357
+ decode: true,
358
+ sort: true,
359
+ arrayFormat: 'none',
360
+ arrayFormatSeparator: ',',
361
+ parseNumbers: false,
362
+ parseBooleans: false,
363
+ types: Object.create(null),
364
+ ...options,
365
+ };
366
+
367
+ validateArrayFormatSeparator(options.arrayFormatSeparator);
368
+
369
+ const formatter = parserForArrayFormat(options);
370
+
371
+ // Create an object with no prototype
372
+ const returnValue = Object.create(null);
373
+
374
+ if (typeof query !== 'string') {
375
+ return returnValue;
376
+ }
377
+
378
+ query = query.trim().replace(/^[?#&]/, '');
379
+
380
+ if (!query) {
381
+ return returnValue;
382
+ }
383
+
384
+ for (const parameter of query.split('&')) {
385
+ if (parameter === '') {
386
+ continue;
387
+ }
388
+
389
+ const parameter_ = options.decode ? parameter.replaceAll('+', ' ') : parameter;
390
+
391
+ let [key, value] = splitOnFirst(parameter_, '=');
392
+
393
+ if (key === undefined) {
394
+ key = parameter_;
395
+ }
396
+
397
+ // Missing `=` should be `null`:
398
+ // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
399
+ value = value === undefined ? null : (['comma', 'separator', 'bracket-separator'].includes(options.arrayFormat) ? value : decode(value, options));
400
+ formatter(decode(key, options), value, returnValue);
401
+ }
402
+
403
+ for (const [key, value] of Object.entries(returnValue)) {
404
+ if (typeof value === 'object' && value !== null && options.types[key] !== 'string') {
405
+ for (const [key2, value2] of Object.entries(value)) {
406
+ const typeOption = options.types[key];
407
+ const type = typeof typeOption === 'function' ? typeOption : (typeOption ? typeOption.replace('[]', '') : undefined);
408
+ value[key2] = parseValue(value2, options, type);
409
+ }
410
+ } else if (typeof value === 'object' && value !== null && options.types[key] === 'string') {
411
+ returnValue[key] = Object.values(value).join(options.arrayFormatSeparator);
412
+ } else {
413
+ returnValue[key] = parseValue(value, options, options.types[key]);
414
+ }
415
+ }
416
+
417
+ if (options.sort === false) {
418
+ return returnValue;
419
+ }
420
+
421
+ // TODO: Remove the use of `reduce`.
422
+ // eslint-disable-next-line unicorn/no-array-reduce
423
+ return (options.sort === true ? Object.keys(returnValue).sort() : Object.keys(returnValue).sort(options.sort)).reduce((result, key) => {
424
+ const value = returnValue[key];
425
+ result[key] = Boolean(value) && typeof value === 'object' && !Array.isArray(value) ? keysSorter(value) : value;
426
+ return result;
427
+ }, Object.create(null));
428
+ }
429
+
430
+ export function stringify(object, options) {
431
+ if (!object) {
432
+ return '';
433
+ }
434
+
435
+ options = {
436
+ encode: true,
437
+ strict: true,
438
+ arrayFormat: 'none',
439
+ arrayFormatSeparator: ',',
440
+ ...options,
441
+ };
442
+
443
+ validateArrayFormatSeparator(options.arrayFormatSeparator);
444
+
445
+ const shouldFilter = key => (
446
+ (options.skipNull && isNullOrUndefined(object[key]))
447
+ || (options.skipEmptyString && object[key] === '')
448
+ );
449
+
450
+ const formatter = encoderForArrayFormat(options);
451
+
452
+ const objectCopy = {};
453
+
454
+ for (const [key, value] of Object.entries(object)) {
455
+ if (!shouldFilter(key)) {
456
+ objectCopy[key] = value;
457
+ }
458
+ }
459
+
460
+ const keys = Object.keys(objectCopy);
461
+
462
+ if (options.sort !== false) {
463
+ keys.sort(options.sort);
464
+ }
465
+
466
+ return keys.map(key => {
467
+ let value = object[key];
468
+
469
+ // Apply replacer function if provided
470
+ if (options.replacer) {
471
+ value = options.replacer(key, value);
472
+
473
+ // If replacer returns undefined, skip this key
474
+ if (value === undefined) {
475
+ return '';
476
+ }
477
+ }
478
+
479
+ if (value === undefined) {
480
+ return '';
481
+ }
482
+
483
+ if (value === null) {
484
+ return encode(key, options);
485
+ }
486
+
487
+ if (Array.isArray(value)) {
488
+ if (value.length === 0 && options.arrayFormat === 'bracket-separator') {
489
+ return encode(key, options) + '[]';
490
+ }
491
+
492
+ // Apply replacer to array elements if provided
493
+ // Note: We don't re-apply replacer to the array itself, only to elements
494
+ let processedArray = value;
495
+ if (options.replacer) {
496
+ processedArray = value.map((item, index) =>
497
+ options.replacer(`${key}[${index}]`, item),
498
+ ).filter(item => item !== undefined);
499
+ }
500
+
501
+ return processedArray
502
+ .reduce(formatter(key), [])
503
+ .join('&');
504
+ }
505
+
506
+ return encode(key, options) + '=' + encode(value, options);
507
+ }).filter(x => x.length > 0).join('&');
508
+ }
509
+
510
+ export function parseUrl(url, options) {
511
+ options = {
512
+ decode: true,
513
+ ...options,
514
+ };
515
+
516
+ let [url_, hash] = splitOnFirst(url, '#');
517
+
518
+ if (url_ === undefined) {
519
+ url_ = url;
520
+ }
521
+
522
+ return {
523
+ url: url_?.split('?')?.[0] ?? '',
524
+ query: parse(extract(url), options),
525
+ ...(options && options.parseFragmentIdentifier && hash ? {fragmentIdentifier: decode(hash, options)} : {}),
526
+ };
527
+ }
528
+
529
+ export function stringifyUrl(object, options) {
530
+ options = {
531
+ encode: true,
532
+ strict: true,
533
+ [encodeFragmentIdentifier]: true,
534
+ ...options,
535
+ };
536
+
537
+ const url = removeHash(object.url).split('?')[0] || '';
538
+ const queryFromUrl = extract(object.url);
539
+
540
+ const query = {
541
+ ...parse(queryFromUrl, {sort: false, ...options}),
542
+ ...object.query,
543
+ };
544
+
545
+ let queryString = stringify(query, options);
546
+ queryString &&= `?${queryString}`;
547
+
548
+ let hash = getHash(object.url);
549
+ if (typeof object.fragmentIdentifier === 'string') {
550
+ const urlObjectForFragmentEncode = new URL(url);
551
+ urlObjectForFragmentEncode.hash = object.fragmentIdentifier;
552
+ hash = options[encodeFragmentIdentifier] ? urlObjectForFragmentEncode.hash : `#${object.fragmentIdentifier}`;
553
+ }
554
+
555
+ return `${url}${queryString}${hash}`;
556
+ }
557
+
558
+ export function pick(input, filter, options) {
559
+ options = {
560
+ parseFragmentIdentifier: true,
561
+ [encodeFragmentIdentifier]: false,
562
+ ...options,
563
+ };
564
+
565
+ const {url, query, fragmentIdentifier} = parseUrl(input, options);
566
+
567
+ return stringifyUrl({
568
+ url,
569
+ query: includeKeys(query, filter),
570
+ fragmentIdentifier,
571
+ }, options);
572
+ }
573
+
574
+ export function exclude(input, filter, options) {
575
+ const exclusionFilter = Array.isArray(filter) ? key => !filter.includes(key) : (key, value) => !filter(key, value);
576
+
577
+ return pick(input, exclusionFilter, options);
578
+ }