@v4fire/core
Version:
V4Fire core library
298 lines (238 loc) • 6.23 kB
text/typescript
/*!
* V4Fire Core
* https://github.com/V4Fire/Core
*
* Released under the MIT license
* https://github.com/V4Fire/Core/blob/master/LICENSE
*/
import { convertIfDate } from 'core/json';
import { defaultToQueryStringParamsFilter } from 'core/url/const';
import type { ToQueryStringOptions, FromQueryStringOptions } from 'core/url/interface';
export * from 'core/url/interface';
/**
* Creates a querystring from the specified data and returns it
*
* @param data
* @param [encode] - if false, then the result string won't be encoded by using encodeURIComponent
*
* @example
* ```js
* // '?a=1'
* toQueryString({a: 1});
* ```
*/
export function toQueryString(data: unknown, encode?: boolean): string;
/**
* Creates a querystring from the specified data and returns it
*
* @param data
* @param opts - additional options
*
* @example
* ```js
* // '?a[]=1&a[]=2'
* toQueryString({a: [1, 2]}, {arraySyntax: true});
* ```
*/
export function toQueryString(data: unknown, opts: ToQueryStringOptions): string;
export function toQueryString(data: unknown, optsOrEncode?: ToQueryStringOptions | boolean): string {
if (!Object.isDictionary(data)) {
return Object.isString(data) ? data : '';
}
let
opts: ToQueryStringOptions;
if (Object.isPlainObject(optsOrEncode)) {
opts = optsOrEncode;
} else {
opts = {encode: optsOrEncode};
}
const
separator = opts.separator ?? '_',
// eslint-disable-next-line @typescript-eslint/unbound-method
paramsFilter = opts.paramsFilter ?? defaultToQueryStringParamsFilter;
const stack = Object.keys(data)
.sort()
.reverse()
.map((key) => ({
key,
el: data[key],
checked: false
}));
const dictionaryKeyTransformer = opts.arraySyntax ?
(baseKey, additionalKey) => `${baseKey}[${additionalKey}]` :
(baseKey, additionalKey) => `${baseKey}${separator}${additionalKey}`;
const arrayKeyTransformer = opts.arraySyntax ?
(baseKey) => `${baseKey}[]` :
(baseKey) => baseKey;
let
res = '';
while (stack.length > 0) {
const
item = stack.pop();
if (item == null) {
continue;
}
const
{el, key} = item;
if (Object.isDictionary(el)) {
const keys = Object.keys(el).sort();
if (keys.length > 0) {
for (let i = keys.length - 1; i >= 0; i--) {
checkAndPush(item, keys[i], dictionaryKeyTransformer);
}
continue;
}
}
if (Object.isArray(el) && el.length > 0) {
for (let key = el.length - 1; key >= 0; key--) {
checkAndPush(item, key, arrayKeyTransformer);
}
continue;
}
if (item.checked || Object.isTruly(paramsFilter(el, key))) {
let
data;
if (Object.isDictionary(el)) {
data = '';
} else {
data = String(el ?? '');
if (opts.encode !== false) {
data = encodeURIComponent(data);
}
}
res += `&${key}=${data}`;
}
}
return res.substr(1);
function checkAndPush(item: typeof stack[0], key: unknown, keyTransformer: Function): void {
const
normalizedKey = String(key),
nextLvlKey = keyTransformer(item.key, normalizedKey),
el = Object.get(item.el, [key]);
if (Object.isTruly(paramsFilter(el, normalizedKey, nextLvlKey))) {
stack.push({
key: nextLvlKey,
el,
checked: true
});
}
}
}
const
isInvalidKey = /\b__proto__\b/,
arraySyntaxRgxp = /\[([^\]]*)]/g,
normalizeURLRgxp = /^(?:[^?]*\?|(?:\w+:)?\/\/.*)/;
/**
* Creates a dictionary from the specified querystring and returns it
*
* @param query
* @param [decode] - if false, then the passed string won't be decoded by using decodeURIComponent
*
* @example
* ```js
* // {a: 1}
* fromQueryString('?a=1');
* ```
*/
export function fromQueryString(query: string, decode?: boolean): Dictionary;
/**
* Creates a dictionary from the specified querystring and returns it
*
* @param query
* @param opts - additional options
*
* @example
* ```js
* // {a: [1, 2]}
* fromQueryString('?a[]=1&a[]=2', {arraySyntax: true});
* ```
*/
export function fromQueryString(query: string, opts: FromQueryStringOptions): Dictionary;
/**
* Creates a dictionary from the specified querystring and returns it.
* This overload doesn't convert key values from a string.
*
* @param query
* @param opts - additional options
*
* @example
* ```js
* // {a: '1'}
* fromQueryString('?a=1', {convert: false});
* ```
*/
export function fromQueryString(
query: string,
opts: {convert: false} & FromQueryStringOptions
): Dictionary<string | null>;
export function fromQueryString(
query: string,
optsOrDecode?: FromQueryStringOptions | boolean
): Dictionary<string | null> {
const
queryObj = {};
query = query.replace(normalizeURLRgxp, '');
if (query === '') {
return queryObj;
}
let
opts: FromQueryStringOptions;
if (Object.isPlainObject(optsOrDecode)) {
opts = optsOrDecode;
} else {
opts = {decode: optsOrDecode};
}
const objOpts = {
separator: opts.arraySyntax ? ']' : opts.separator
};
const
indices = Object.createDict<number>(),
variables = query.split('&');
for (let i = 0; i < variables.length; i++) {
let
[key, val = null] = variables[i].split('=');
if (opts.decode !== false) {
key = decodeURIComponent(key);
if (val != null) {
val = decodeURIComponent(val);
}
}
if (opts.arraySyntax) {
let
path = '',
nestedArray = false;
key = key.replace(arraySyntaxRgxp, (str, prop, lastIndex) => {
if (path === '') {
path += key.slice(0, lastIndex);
}
path += str;
if (prop === '') {
if (nestedArray) {
prop = '0';
} else {
prop = indices[path] ?? '0';
indices[path] = Number(prop) + 1;
}
nestedArray = true;
}
return `]${prop}`;
});
}
const
oldVal = objOpts.separator != null ? Object.get(queryObj, key, objOpts) : queryObj[key];
let
normalizedVal = opts.convert !== false ? Object.parse(val, convertIfDate) : val;
if (oldVal !== undefined) {
normalizedVal = Array.concat([], oldVal, Object.isArray(normalizedVal) ? [normalizedVal] : normalizedVal);
}
if (isInvalidKey.test(key)) {
continue;
}
if (objOpts.separator != null) {
Object.set(queryObj, key, normalizedVal, objOpts);
} else {
queryObj[key] = normalizedVal;
}
}
return queryObj;
}