UNPKG

@ffsm/serialize

Version:
182 lines (181 loc) 5.58 kB
import Nullish from '@ffsm/nullish'; import { decode as _decode } from './decode'; function parseKeyPath(key) { const path = []; let currentKey = ''; let inBracket = false; for (let i = 0; i < key.length; i++) { const char = key[i]; if (char === '[' && !inBracket) { path.push(currentKey); currentKey = ''; inBracket = true; } else if (char === ']' && inBracket) { path.push(currentKey); currentKey = ''; inBracket = false; } else { currentKey += char; } } if (currentKey) { path.push(currentKey); } return path.filter(Boolean); } function addValueToPath(obj, path, value, isArray) { if (Nullish.isNullishOrEmpty(path)) { return; } const key = path[0]; if (path.length === 1) { if (isArray) { if (!Array.isArray(obj[key])) { obj[key] = []; } obj[key].push(value); } else { obj[key] = value; } return; } const isNextKeyNumber = !Number.isNaN(Number(path[1])); if (isNextKeyNumber) { if (!obj[key] || !Array.isArray(obj[key])) { obj[key] = []; } } else { if (!obj[key] || Array.isArray(obj[key]) || typeof obj[key] !== 'object') { obj[key] = {}; } } addValueToPath(obj[key], path.slice(1), value, isArray); } function setNestedValue(obj, path, value) { if (Nullish.isNullishOrEmpty(path)) { return; } if (path.length === 1) { obj[path[0]] = value; return; } const key = path[0]; const isNextKeyNumber = !Number.isNaN(Number(path[1])); if (isNextKeyNumber) { if (!obj[key] || !Array.isArray(obj[key])) { obj[key] = []; } } else { if (!obj[key] || Array.isArray(obj[key]) || typeof obj[key] !== 'object') { obj[key] = {}; } } setNestedValue(obj[key], path.slice(1), value); } /** * Converts a URL query string into an object. * * This function parses a query string and transforms it into a structured object, * handling various formats of arrays, nested objects, and primitive values. * * @param queryString - The query string to parse (with or without the leading '?') * @param options - Configuration options for query string parsing * @returns A structured object representing the parsed query string * * @example * ```typescript * // Basic usage * parse('name=John&age=30'); * // { name: 'John', age: '30' } * * // With number and boolean parsing * parse('active=true&count=42', { parseBooleans: true, parseNumbers: true }); * // { active: true, count: 42 } * * // With arrays in bracket format * parse('colors[]=red&colors[]=blue', { arrayFormat: 'bracket' }); * // { colors: ['red', 'blue'] } * * // With arrays in index format * parse('colors[0]=red&colors[1]=blue', { arrayFormat: 'index' }); * // { colors: ['red', 'blue'] } * * // With arrays in comma format * parse('colors=red,blue', { arrayFormat: 'comma' }); * // { colors: ['red', 'blue'] } * * // With nested objects * parse('user[name]=John&user[profile][age]=30'); * // { user: { name: 'John', profile: { age: '30' } } } * ``` */ export function parse(queryString, options = {}) { if (Nullish.isNullishOrEmpty(queryString)) { return {}; } const { arrayFormat = 'none', arrayFormatSeparator = ',', parseNumbers = false, parseBooleans = false, decode = true, ignorePrefix = true, } = options; let query = queryString; if (ignorePrefix) { query = query.replace(/^[\?\#]/, ''); } const decoder = (value) => { if (!decode) return value; return _decode(value, decode); }; function parseValue(value) { if (Nullish.isNullishOrEmpty(value)) { return ''; } if (parseBooleans) { if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; } if (parseNumbers) { if (/^-?(\d+\.?\d*|\.\d+)$/.test(value)) { const num = Number(value); if (!Number.isNaN(num) && Number.isFinite(num)) return num; } } return decoder(value); } const pairs = query.split('&').filter(Boolean); const result = {}; for (const pair of pairs) { const [rawKey, rawValue] = pair.split('=', 2); const key = decoder(rawKey); const value = decoder(rawValue); if (!key) { continue; } let path; const parsedValue = parseValue(value); if (key.endsWith('[]') && arrayFormat === 'bracket') { path = [key.slice(0, -2)]; addValueToPath(result, path, parsedValue, true); } else if (key.includes('[') && key.endsWith(']')) { path = parseKeyPath(key); addValueToPath(result, path, parsedValue, true); } else if ((arrayFormat === 'comma' || arrayFormat === 'separator') && value.includes(arrayFormatSeparator)) { path = [key]; const values = value.split(arrayFormatSeparator).map(parseValue); setNestedValue(result, path, values); } else { path = [key]; addValueToPath(result, path, parsedValue, false); } } return result; }