1'use strict'; 2const strictUriEncode = require('strict-uri-encode'); 3const decodeComponent = require('decode-uri-component'); 4const splitOnFirst = require('split-on-first'); 5 6function encoderForArrayFormat(options) { 7 switch (options.arrayFormat) { 8 case 'index': 9 return key => (result, value) => { 10 const index = result.length; 11 if (value === undefined) { 12 return result; 13 } 14 15 if (value === null) { 16 return [...result, [encode(key, options), '[', index, ']'].join('')]; 17 } 18 19 return [ 20 ...result, 21 [encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join('') 22 ]; 23 }; 24 25 case 'bracket': 26 return key => (result, value) => { 27 if (value === undefined) { 28 return result; 29 } 30 31 if (value === null) { 32 return [...result, [encode(key, options), '[]'].join('')]; 33 } 34 35 return [...result, [encode(key, options), '[]=', encode(value, options)].join('')]; 36 }; 37 38 case 'comma': 39 return key => (result, value, index) => { 40 if (value === null || value === undefined || value.length === 0) { 41 return result; 42 } 43 44 if (index === 0) { 45 return [[encode(key, options), '=', encode(value, options)].join('')]; 46 } 47 48 return [[result, encode(value, options)].join(',')]; 49 }; 50 51 default: 52 return key => (result, value) => { 53 if (value === undefined) { 54 return result; 55 } 56 57 if (value === null) { 58 return [...result, encode(key, options)]; 59 } 60 61 return [...result, [encode(key, options), '=', encode(value, options)].join('')]; 62 }; 63 } 64} 65 66function parserForArrayFormat(options) { 67 let result; 68 69 switch (options.arrayFormat) { 70 case 'index': 71 return (key, value, accumulator) => { 72 result = /\[(\d*)\]$/.exec(key); 73 74 key = key.replace(/\[\d*\]$/, ''); 75 76 if (!result) { 77 accumulator[key] = value; 78 return; 79 } 80 81 if (accumulator[key] === undefined) { 82 accumulator[key] = {}; 83 } 84 85 accumulator[key][result[1]] = value; 86 }; 87 88 case 'bracket': 89 return (key, value, accumulator) => { 90 result = /(\[\])$/.exec(key); 91 key = key.replace(/\[\]$/, ''); 92 93 if (!result) { 94 accumulator[key] = value; 95 return; 96 } 97 98 if (accumulator[key] === undefined) { 99 accumulator[key] = [value]; 100 return; 101 } 102 103 accumulator[key] = [].concat(accumulator[key], value); 104 }; 105 106 case 'comma': 107 return (key, value, accumulator) => { 108 const isArray = typeof value === 'string' && value.split('').indexOf(',') > -1; 109 const newValue = isArray ? value.split(',') : value; 110 accumulator[key] = newValue; 111 }; 112 113 default: 114 return (key, value, accumulator) => { 115 if (accumulator[key] === undefined) { 116 accumulator[key] = value; 117 return; 118 } 119 120 accumulator[key] = [].concat(accumulator[key], value); 121 }; 122 } 123} 124 125function encode(value, options) { 126 if (options.encode) { 127 return options.strict ? strictUriEncode(value) : encodeURIComponent(value); 128 } 129 130 return value; 131} 132 133function decode(value, options) { 134 if (options.decode) { 135 return decodeComponent(value); 136 } 137 138 return value; 139} 140 141function keysSorter(input) { 142 if (Array.isArray(input)) { 143 return input.sort(); 144 } 145 146 if (typeof input === 'object') { 147 return keysSorter(Object.keys(input)) 148 .sort((a, b) => Number(a) - Number(b)) 149 .map(key => input[key]); 150 } 151 152 return input; 153} 154 155function removeHash(input) { 156 const hashStart = input.indexOf('#'); 157 if (hashStart !== -1) { 158 input = input.slice(0, hashStart); 159 } 160 161 return input; 162} 163 164function extract(input) { 165 input = removeHash(input); 166 const queryStart = input.indexOf('?'); 167 if (queryStart === -1) { 168 return ''; 169 } 170 171 return input.slice(queryStart + 1); 172} 173 174function parse(input, options) { 175 options = Object.assign({ 176 decode: true, 177 sort: true, 178 arrayFormat: 'none', 179 parseNumbers: false, 180 parseBooleans: false 181 }, options); 182 183 const formatter = parserForArrayFormat(options); 184 185 // Create an object with no prototype 186 const ret = Object.create(null); 187 188 if (typeof input !== 'string') { 189 return ret; 190 } 191 192 input = input.trim().replace(/^[?#&]/, ''); 193 194 if (!input) { 195 return ret; 196 } 197 198 for (const param of input.split('&')) { 199 let [key, value] = splitOnFirst(param.replace(/\+/g, ' '), '='); 200 201 // Missing `=` should be `null`: 202 // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters 203 value = value === undefined ? null : decode(value, options); 204 205 if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) { 206 value = Number(value); 207 } else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) { 208 value = value.toLowerCase() === 'true'; 209 } 210 211 formatter(decode(key, options), value, ret); 212 } 213 214 if (options.sort === false) { 215 return ret; 216 } 217 218 return (options.sort === true ? Object.keys(ret).sort() : Object.keys(ret).sort(options.sort)).reduce((result, key) => { 219 const value = ret[key]; 220 if (Boolean(value) && typeof value === 'object' && !Array.isArray(value)) { 221 // Sort object keys, not values 222 result[key] = keysSorter(value); 223 } else { 224 result[key] = value; 225 } 226 227 return result; 228 }, Object.create(null)); 229} 230 231exports.extract = extract; 232exports.parse = parse; 233 234exports.stringify = (object, options) => { 235 if (!object) { 236 return ''; 237 } 238 239 options = Object.assign({ 240 encode: true, 241 strict: true, 242 arrayFormat: 'none' 243 }, options); 244 245 const formatter = encoderForArrayFormat(options); 246 const keys = Object.keys(object); 247 248 if (options.sort !== false) { 249 keys.sort(options.sort); 250 } 251 252 return keys.map(key => { 253 const value = object[key]; 254 255 if (value === undefined) { 256 return ''; 257 } 258 259 if (value === null) { 260 return encode(key, options); 261 } 262 263 if (Array.isArray(value)) { 264 return value 265 .reduce(formatter(key), []) 266 .join('&'); 267 } 268 269 return encode(key, options) + '=' + encode(value, options); 270 }).filter(x => x.length > 0).join('&'); 271}; 272 273exports.parseUrl = (input, options) => { 274 return { 275 url: removeHash(input).split('?')[0] || '', 276 query: parse(extract(input), options) 277 }; 278}; 279