UNPKG

query-string

Version:
557 lines (444 loc) 13.3 kB
import decodeComponent from 'decode-uri-component'; import {includeKeys} from 'filter-obj'; import splitOnFirst from 'split-on-first'; const isNullOrUndefined = value => value === null || value === undefined; // eslint-disable-next-line unicorn/prefer-code-point const strictUriEncode = string => encodeURIComponent(string).replaceAll(/[!'()*]/g, x => `%${x.charCodeAt(0).toString(16).toUpperCase()}`); const encodeFragmentIdentifier = Symbol('encodeFragmentIdentifier'); function encoderForArrayFormat(options) { switch (options.arrayFormat) { case 'index': { return key => (result, value) => { const index = result.length; if ( value === undefined || (options.skipNull && value === null) || (options.skipEmptyString && value === '') ) { return result; } if (value === null) { return [ ...result, [encode(key, options), '[', index, ']'].join(''), ]; } return [ ...result, [encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join(''), ]; }; } case 'bracket': { return key => (result, value) => { if ( value === undefined || (options.skipNull && value === null) || (options.skipEmptyString && value === '') ) { return result; } if (value === null) { return [ ...result, [encode(key, options), '[]'].join(''), ]; } return [ ...result, [encode(key, options), '[]=', encode(value, options)].join(''), ]; }; } case 'colon-list-separator': { return key => (result, value) => { if ( value === undefined || (options.skipNull && value === null) || (options.skipEmptyString && value === '') ) { return result; } if (value === null) { return [ ...result, [encode(key, options), ':list='].join(''), ]; } return [ ...result, [encode(key, options), ':list=', encode(value, options)].join(''), ]; }; } case 'comma': case 'separator': case 'bracket-separator': { const keyValueSeparator = options.arrayFormat === 'bracket-separator' ? '[]=' : '='; return key => (result, value) => { if ( value === undefined || (options.skipNull && value === null) || (options.skipEmptyString && value === '') ) { return result; } // Translate null to an empty string so that it doesn't serialize as 'null' value = value === null ? '' : value; if (result.length === 0) { return [[encode(key, options), keyValueSeparator, encode(value, options)].join('')]; } return [[result, encode(value, options)].join(options.arrayFormatSeparator)]; }; } default: { return key => (result, value) => { if ( value === undefined || (options.skipNull && value === null) || (options.skipEmptyString && value === '') ) { return result; } if (value === null) { return [ ...result, encode(key, options), ]; } return [ ...result, [encode(key, options), '=', encode(value, options)].join(''), ]; }; } } } function parserForArrayFormat(options) { let result; switch (options.arrayFormat) { case 'index': { return (key, value, accumulator) => { result = /\[(\d*)]$/.exec(key); key = key.replace(/\[\d*]$/, ''); if (!result) { accumulator[key] = value; return; } if (accumulator[key] === undefined) { accumulator[key] = {}; } accumulator[key][result[1]] = value; }; } case 'bracket': { return (key, value, accumulator) => { result = /(\[])$/.exec(key); key = key.replace(/\[]$/, ''); if (!result) { accumulator[key] = value; return; } if (accumulator[key] === undefined) { accumulator[key] = [value]; return; } accumulator[key] = [...accumulator[key], value]; }; } case 'colon-list-separator': { return (key, value, accumulator) => { result = /(:list)$/.exec(key); key = key.replace(/:list$/, ''); if (!result) { accumulator[key] = value; return; } if (accumulator[key] === undefined) { accumulator[key] = [value]; return; } accumulator[key] = [...accumulator[key], value]; }; } case 'comma': case 'separator': { return (key, value, accumulator) => { 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; }; } case 'bracket-separator': { return (key, value, accumulator) => { const isArray = /(\[])$/.test(key); key = key.replace(/\[]$/, ''); if (!isArray) { accumulator[key] = value ? decode(value, options) : value; return; } const arrayValue = value === null ? [] : decode(value, options).split(options.arrayFormatSeparator); if (accumulator[key] === undefined) { accumulator[key] = arrayValue; return; } accumulator[key] = [...accumulator[key], ...arrayValue]; }; } default: { return (key, value, accumulator) => { if (accumulator[key] === undefined) { accumulator[key] = value; return; } accumulator[key] = [...[accumulator[key]].flat(), value]; }; } } } function validateArrayFormatSeparator(value) { if (typeof value !== 'string' || value.length !== 1) { throw new TypeError('arrayFormatSeparator must be single character string'); } } function encode(value, options) { if (options.encode) { return options.strict ? strictUriEncode(value) : encodeURIComponent(value); } return value; } function decode(value, options) { if (options.decode) { return decodeComponent(value); } return value; } function keysSorter(input) { if (Array.isArray(input)) { return input.sort(); } if (typeof input === 'object') { return keysSorter(Object.keys(input)) .sort((a, b) => Number(a) - Number(b)) .map(key => input[key]); } return input; } function removeHash(input) { const hashStart = input.indexOf('#'); if (hashStart !== -1) { input = input.slice(0, hashStart); } return input; } function getHash(url) { let hash = ''; const hashStart = url.indexOf('#'); if (hashStart !== -1) { hash = url.slice(hashStart); } return hash; } function parseValue(value, options, type) { if (type === 'string' && typeof value === 'string') { return value; } if (typeof type === 'function' && typeof value === 'string') { return type(value); } if (type === 'boolean' && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) { return value.toLowerCase() === 'true'; } if (type === 'boolean' && value !== null && (value.toLowerCase() === '1' || value.toLowerCase() === '0')) { return value.toLowerCase() === '1'; } if (type === 'string[]' && options.arrayFormat !== 'none' && typeof value === 'string') { return [value]; } if (type === 'number[]' && options.arrayFormat !== 'none' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) { return [Number(value)]; } if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) { return Number(value); } if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) { return value.toLowerCase() === 'true'; } if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) { return Number(value); } return value; } export function extract(input) { input = removeHash(input); const queryStart = input.indexOf('?'); if (queryStart === -1) { return ''; } return input.slice(queryStart + 1); } export function parse(query, options) { options = { decode: true, sort: true, arrayFormat: 'none', arrayFormatSeparator: ',', parseNumbers: false, parseBooleans: false, types: Object.create(null), ...options, }; validateArrayFormatSeparator(options.arrayFormatSeparator); const formatter = parserForArrayFormat(options); // Create an object with no prototype const returnValue = Object.create(null); if (typeof query !== 'string') { return returnValue; } query = query.trim().replace(/^[?#&]/, ''); if (!query) { return returnValue; } for (const parameter of query.split('&')) { if (parameter === '') { continue; } const parameter_ = options.decode ? parameter.replaceAll('+', ' ') : parameter; let [key, value] = splitOnFirst(parameter_, '='); if (key === undefined) { key = parameter_; } // Missing `=` should be `null`: // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters value = value === undefined ? null : (['comma', 'separator', 'bracket-separator'].includes(options.arrayFormat) ? value : decode(value, options)); formatter(decode(key, options), value, returnValue); } for (const [key, value] of Object.entries(returnValue)) { if (typeof value === 'object' && value !== null && options.types[key] !== 'string') { for (const [key2, value2] of Object.entries(value)) { const type = options.types[key] ? options.types[key].replace('[]', '') : undefined; value[key2] = parseValue(value2, options, type); } } else if (typeof value === 'object' && value !== null && options.types[key] === 'string') { returnValue[key] = Object.values(value).join(options.arrayFormatSeparator); } else { returnValue[key] = parseValue(value, options, options.types[key]); } } if (options.sort === false) { return returnValue; } // TODO: Remove the use of `reduce`. // eslint-disable-next-line unicorn/no-array-reduce return (options.sort === true ? Object.keys(returnValue).sort() : Object.keys(returnValue).sort(options.sort)).reduce((result, key) => { const value = returnValue[key]; result[key] = Boolean(value) && typeof value === 'object' && !Array.isArray(value) ? keysSorter(value) : value; return result; }, Object.create(null)); } export function stringify(object, options) { if (!object) { return ''; } options = { encode: true, strict: true, arrayFormat: 'none', arrayFormatSeparator: ',', ...options, }; validateArrayFormatSeparator(options.arrayFormatSeparator); const shouldFilter = key => ( (options.skipNull && isNullOrUndefined(object[key])) || (options.skipEmptyString && object[key] === '') ); const formatter = encoderForArrayFormat(options); const objectCopy = {}; for (const [key, value] of Object.entries(object)) { if (!shouldFilter(key)) { objectCopy[key] = value; } } const keys = Object.keys(objectCopy); if (options.sort !== false) { keys.sort(options.sort); } return keys.map(key => { const value = object[key]; if (value === undefined) { return ''; } if (value === null) { return encode(key, options); } if (Array.isArray(value)) { if (value.length === 0 && options.arrayFormat === 'bracket-separator') { return encode(key, options) + '[]'; } return value .reduce(formatter(key), []) .join('&'); } return encode(key, options) + '=' + encode(value, options); }).filter(x => x.length > 0).join('&'); } export function parseUrl(url, options) { options = { decode: true, ...options, }; let [url_, hash] = splitOnFirst(url, '#'); if (url_ === undefined) { url_ = url; } return { url: url_?.split('?')?.[0] ?? '', query: parse(extract(url), options), ...(options && options.parseFragmentIdentifier && hash ? {fragmentIdentifier: decode(hash, options)} : {}), }; } export function stringifyUrl(object, options) { options = { encode: true, strict: true, [encodeFragmentIdentifier]: true, ...options, }; const url = removeHash(object.url).split('?')[0] || ''; const queryFromUrl = extract(object.url); const query = { ...parse(queryFromUrl, {sort: false, ...options}), ...object.query, }; let queryString = stringify(query, options); queryString &&= `?${queryString}`; let hash = getHash(object.url); if (typeof object.fragmentIdentifier === 'string') { const urlObjectForFragmentEncode = new URL(url); urlObjectForFragmentEncode.hash = object.fragmentIdentifier; hash = options[encodeFragmentIdentifier] ? urlObjectForFragmentEncode.hash : `#${object.fragmentIdentifier}`; } return `${url}${queryString}${hash}`; } export function pick(input, filter, options) { options = { parseFragmentIdentifier: true, [encodeFragmentIdentifier]: false, ...options, }; const {url, query, fragmentIdentifier} = parseUrl(input, options); return stringifyUrl({ url, query: includeKeys(query, filter), fragmentIdentifier, }, options); } export function exclude(input, filter, options) { const exclusionFilter = Array.isArray(filter) ? key => !filter.includes(key) : (key, value) => !filter(key, value); return pick(input, exclusionFilter, options); }