| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- 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 (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
- return value.toLowerCase() === 'true';
- }
- if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
- return Number(value);
- }
- 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}),
- ...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);
- }
|