src/object/fromQueryString.ts
import isNil from '~/is/isNil'
import isNumeric from '~/is/isNumeric'
import type { TextNumber } from '~/internal/types'
const queryRe = /^\?/
const plusRe = /\+/g
const keyRe = /(\[):?([^\]]*)\]/g
const nameRe = /^([^[]+)/ // eslint-disable-line no-useless-escape
type FromQueryStringOptions = {
decodeName: boolean
}
/**
* Converts a query string back into an object.
*
* Non-recursive:
*
* fromQueryString("foo=1&bar=2"); // returns {foo: '1', bar: '2'}
* fromQueryString("foo=&bar=2"); // returns {foo: '', bar: '2'}
* fromQueryString("some%20price=%24300"); // returns {'some price': '$300'}
* fromQueryString("colors=red&colors=green&colors=blue"); // returns {colors: ['red', 'green', 'blue']}
*
* Recursive:
*
* fromQueryString(
* "username=Jacky&"+
* "dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&"+
* "hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&"+
* "hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
*
* // returns
* {
* username: 'Jacky',
* dateOfBirth: {
* day: '1',
* month: '2',
* year: '1911'
* },
* hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
* }
*
* @param {String|null} queryString The query string to decode
* @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
* @param {Object} options = {
* - decodeName {Boolean} Decode KeyNames in the queryString
* }
* PHP / Ruby on Rails servers and similar.
* @return {Object}
* @todo write tests
*/
export default function fromQueryString(
queryString: string,
recursive: boolean = false,
options: FromQueryStringOptions = { decodeName: true }
): Record<string, any> {
if (isNil(queryString)) {
return {}
}
let parts = queryString.replace(queryRe, '').split('&'),
object = Object.create(null),
temporary,
components: string[],
name: string,
value,
i,
ln,
part: string,
j,
subLn,
matchedKeys: RegExpMatchArray | null,
matchedName: RegExpMatchArray | null,
keys: string[],
key: string,
nextKey: TextNumber
for (i = 0, ln = parts.length; i < ln; i++) {
part = parts[i]
if (part.length > 0) {
components = part.split('=')
name = components[0]
name = name.replace(plusRe, '%20')
name = options.decodeName ? decodeURIComponent(name) : name
value = components[1]
if (value !== undefined) {
value = value.replace(plusRe, '%20')
value = decodeURIComponent(value)
} else {
value = ''
}
if (!recursive) {
if (Object.prototype.hasOwnProperty.call(object, name)) {
if (!Array.isArray(object[name])) {
object[name] = [object[name]]
}
object[name].push(value)
} else {
object[name] = value
}
} else {
matchedKeys = name.match(keyRe)
matchedName = name.match(nameRe)
//<debug>
if (!matchedName) {
throw new Error('[fromQueryString] Malformed query string given, failed parsing name from "' + part + '"')
}
//</debug>
name = matchedName[0]
keys = []
if (matchedKeys === null) {
object[name] = value
continue
}
for (j = 0, subLn = matchedKeys.length; j < subLn; j++) {
key = matchedKeys[j]
key = key.length === 2 ? '' : key.substring(1, key.length - 1)
keys.push(key)
}
keys.unshift(name)
temporary = object
for (j = 0, subLn = keys.length; j < subLn; j++) {
key = keys[j]
if (j === subLn - 1) {
if (Array.isArray(temporary) && key === '') {
temporary.push(value)
} else {
temporary[key] = value
}
} else {
if (temporary[key] === undefined || typeof temporary[key] === 'string') {
nextKey = keys[j + 1]
temporary[key] = isNumeric(nextKey) || nextKey === '' ? [] : {}
}
temporary = temporary[key]
}
}
}
}
}
return object
}