index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. 'use strict';
  2. const strictUriEncode = require('strict-uri-encode');
  3. const decodeComponent = require('decode-uri-component');
  4. const splitOnFirst = require('split-on-first');
  5. const filterObject = require('filter-obj');
  6. const isNullOrUndefined = value => value === null || value === undefined;
  7. const encodeFragmentIdentifier = Symbol('encodeFragmentIdentifier');
  8. function encoderForArrayFormat(options) {
  9. switch (options.arrayFormat) {
  10. case 'index':
  11. return key => (result, value) => {
  12. const index = result.length;
  13. if (
  14. value === undefined ||
  15. (options.skipNull && value === null) ||
  16. (options.skipEmptyString && value === '')
  17. ) {
  18. return result;
  19. }
  20. if (value === null) {
  21. return [...result, [encode(key, options), '[', index, ']'].join('')];
  22. }
  23. return [
  24. ...result,
  25. [encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join('')
  26. ];
  27. };
  28. case 'bracket':
  29. return key => (result, value) => {
  30. if (
  31. value === undefined ||
  32. (options.skipNull && value === null) ||
  33. (options.skipEmptyString && value === '')
  34. ) {
  35. return result;
  36. }
  37. if (value === null) {
  38. return [...result, [encode(key, options), '[]'].join('')];
  39. }
  40. return [...result, [encode(key, options), '[]=', encode(value, options)].join('')];
  41. };
  42. case 'colon-list-separator':
  43. return key => (result, value) => {
  44. if (
  45. value === undefined ||
  46. (options.skipNull && value === null) ||
  47. (options.skipEmptyString && value === '')
  48. ) {
  49. return result;
  50. }
  51. if (value === null) {
  52. return [...result, [encode(key, options), ':list='].join('')];
  53. }
  54. return [...result, [encode(key, options), ':list=', encode(value, options)].join('')];
  55. };
  56. case 'comma':
  57. case 'separator':
  58. case 'bracket-separator': {
  59. const keyValueSep = options.arrayFormat === 'bracket-separator' ?
  60. '[]=' :
  61. '=';
  62. return key => (result, value) => {
  63. if (
  64. value === undefined ||
  65. (options.skipNull && value === null) ||
  66. (options.skipEmptyString && value === '')
  67. ) {
  68. return result;
  69. }
  70. // Translate null to an empty string so that it doesn't serialize as 'null'
  71. value = value === null ? '' : value;
  72. if (result.length === 0) {
  73. return [[encode(key, options), keyValueSep, encode(value, options)].join('')];
  74. }
  75. return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
  76. };
  77. }
  78. default:
  79. return key => (result, value) => {
  80. if (
  81. value === undefined ||
  82. (options.skipNull && value === null) ||
  83. (options.skipEmptyString && value === '')
  84. ) {
  85. return result;
  86. }
  87. if (value === null) {
  88. return [...result, encode(key, options)];
  89. }
  90. return [...result, [encode(key, options), '=', encode(value, options)].join('')];
  91. };
  92. }
  93. }
  94. function parserForArrayFormat(options) {
  95. let result;
  96. switch (options.arrayFormat) {
  97. case 'index':
  98. return (key, value, accumulator) => {
  99. result = /\[(\d*)\]$/.exec(key);
  100. key = key.replace(/\[\d*\]$/, '');
  101. if (!result) {
  102. accumulator[key] = value;
  103. return;
  104. }
  105. if (accumulator[key] === undefined) {
  106. accumulator[key] = {};
  107. }
  108. accumulator[key][result[1]] = value;
  109. };
  110. case 'bracket':
  111. return (key, value, accumulator) => {
  112. result = /(\[\])$/.exec(key);
  113. key = key.replace(/\[\]$/, '');
  114. if (!result) {
  115. accumulator[key] = value;
  116. return;
  117. }
  118. if (accumulator[key] === undefined) {
  119. accumulator[key] = [value];
  120. return;
  121. }
  122. accumulator[key] = [].concat(accumulator[key], value);
  123. };
  124. case 'colon-list-separator':
  125. return (key, value, accumulator) => {
  126. result = /(:list)$/.exec(key);
  127. key = key.replace(/:list$/, '');
  128. if (!result) {
  129. accumulator[key] = value;
  130. return;
  131. }
  132. if (accumulator[key] === undefined) {
  133. accumulator[key] = [value];
  134. return;
  135. }
  136. accumulator[key] = [].concat(accumulator[key], value);
  137. };
  138. case 'comma':
  139. case 'separator':
  140. return (key, value, accumulator) => {
  141. const isArray = typeof value === 'string' && value.includes(options.arrayFormatSeparator);
  142. const isEncodedArray = (typeof value === 'string' && !isArray && decode(value, options).includes(options.arrayFormatSeparator));
  143. value = isEncodedArray ? decode(value, options) : value;
  144. const newValue = isArray || isEncodedArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : value === null ? value : decode(value, options);
  145. accumulator[key] = newValue;
  146. };
  147. case 'bracket-separator':
  148. return (key, value, accumulator) => {
  149. const isArray = /(\[\])$/.test(key);
  150. key = key.replace(/\[\]$/, '');
  151. if (!isArray) {
  152. accumulator[key] = value ? decode(value, options) : value;
  153. return;
  154. }
  155. const arrayValue = value === null ?
  156. [] :
  157. value.split(options.arrayFormatSeparator).map(item => decode(item, options));
  158. if (accumulator[key] === undefined) {
  159. accumulator[key] = arrayValue;
  160. return;
  161. }
  162. accumulator[key] = [].concat(accumulator[key], arrayValue);
  163. };
  164. default:
  165. return (key, value, accumulator) => {
  166. if (accumulator[key] === undefined) {
  167. accumulator[key] = value;
  168. return;
  169. }
  170. accumulator[key] = [].concat(accumulator[key], value);
  171. };
  172. }
  173. }
  174. function validateArrayFormatSeparator(value) {
  175. if (typeof value !== 'string' || value.length !== 1) {
  176. throw new TypeError('arrayFormatSeparator must be single character string');
  177. }
  178. }
  179. function encode(value, options) {
  180. if (options.encode) {
  181. return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
  182. }
  183. return value;
  184. }
  185. function decode(value, options) {
  186. if (options.decode) {
  187. return decodeComponent(value);
  188. }
  189. return value;
  190. }
  191. function keysSorter(input) {
  192. if (Array.isArray(input)) {
  193. return input.sort();
  194. }
  195. if (typeof input === 'object') {
  196. return keysSorter(Object.keys(input))
  197. .sort((a, b) => Number(a) - Number(b))
  198. .map(key => input[key]);
  199. }
  200. return input;
  201. }
  202. function removeHash(input) {
  203. const hashStart = input.indexOf('#');
  204. if (hashStart !== -1) {
  205. input = input.slice(0, hashStart);
  206. }
  207. return input;
  208. }
  209. function getHash(url) {
  210. let hash = '';
  211. const hashStart = url.indexOf('#');
  212. if (hashStart !== -1) {
  213. hash = url.slice(hashStart);
  214. }
  215. return hash;
  216. }
  217. function extract(input) {
  218. input = removeHash(input);
  219. const queryStart = input.indexOf('?');
  220. if (queryStart === -1) {
  221. return '';
  222. }
  223. return input.slice(queryStart + 1);
  224. }
  225. function parseValue(value, options) {
  226. if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
  227. value = Number(value);
  228. } else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
  229. value = value.toLowerCase() === 'true';
  230. }
  231. return value;
  232. }
  233. function parse(query, options) {
  234. options = Object.assign({
  235. decode: true,
  236. sort: true,
  237. arrayFormat: 'none',
  238. arrayFormatSeparator: ',',
  239. parseNumbers: false,
  240. parseBooleans: false
  241. }, options);
  242. validateArrayFormatSeparator(options.arrayFormatSeparator);
  243. const formatter = parserForArrayFormat(options);
  244. // Create an object with no prototype
  245. const ret = Object.create(null);
  246. if (typeof query !== 'string') {
  247. return ret;
  248. }
  249. query = query.trim().replace(/^[?#&]/, '');
  250. if (!query) {
  251. return ret;
  252. }
  253. for (const param of query.split('&')) {
  254. if (param === '') {
  255. continue;
  256. }
  257. let [key, value] = splitOnFirst(options.decode ? param.replace(/\+/g, ' ') : param, '=');
  258. // Missing `=` should be `null`:
  259. // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
  260. value = value === undefined ? null : ['comma', 'separator', 'bracket-separator'].includes(options.arrayFormat) ? value : decode(value, options);
  261. formatter(decode(key, options), value, ret);
  262. }
  263. for (const key of Object.keys(ret)) {
  264. const value = ret[key];
  265. if (typeof value === 'object' && value !== null) {
  266. for (const k of Object.keys(value)) {
  267. value[k] = parseValue(value[k], options);
  268. }
  269. } else {
  270. ret[key] = parseValue(value, options);
  271. }
  272. }
  273. if (options.sort === false) {
  274. return ret;
  275. }
  276. return (options.sort === true ? Object.keys(ret).sort() : Object.keys(ret).sort(options.sort)).reduce((result, key) => {
  277. const value = ret[key];
  278. if (Boolean(value) && typeof value === 'object' && !Array.isArray(value)) {
  279. // Sort object keys, not values
  280. result[key] = keysSorter(value);
  281. } else {
  282. result[key] = value;
  283. }
  284. return result;
  285. }, Object.create(null));
  286. }
  287. exports.extract = extract;
  288. exports.parse = parse;
  289. exports.stringify = (object, options) => {
  290. if (!object) {
  291. return '';
  292. }
  293. options = Object.assign({
  294. encode: true,
  295. strict: true,
  296. arrayFormat: 'none',
  297. arrayFormatSeparator: ','
  298. }, options);
  299. validateArrayFormatSeparator(options.arrayFormatSeparator);
  300. const shouldFilter = key => (
  301. (options.skipNull && isNullOrUndefined(object[key])) ||
  302. (options.skipEmptyString && object[key] === '')
  303. );
  304. const formatter = encoderForArrayFormat(options);
  305. const objectCopy = {};
  306. for (const key of Object.keys(object)) {
  307. if (!shouldFilter(key)) {
  308. objectCopy[key] = object[key];
  309. }
  310. }
  311. const keys = Object.keys(objectCopy);
  312. if (options.sort !== false) {
  313. keys.sort(options.sort);
  314. }
  315. return keys.map(key => {
  316. const value = object[key];
  317. if (value === undefined) {
  318. return '';
  319. }
  320. if (value === null) {
  321. return encode(key, options);
  322. }
  323. if (Array.isArray(value)) {
  324. if (value.length === 0 && options.arrayFormat === 'bracket-separator') {
  325. return encode(key, options) + '[]';
  326. }
  327. return value
  328. .reduce(formatter(key), [])
  329. .join('&');
  330. }
  331. return encode(key, options) + '=' + encode(value, options);
  332. }).filter(x => x.length > 0).join('&');
  333. };
  334. exports.parseUrl = (url, options) => {
  335. options = Object.assign({
  336. decode: true
  337. }, options);
  338. const [url_, hash] = splitOnFirst(url, '#');
  339. return Object.assign(
  340. {
  341. url: url_.split('?')[0] || '',
  342. query: parse(extract(url), options)
  343. },
  344. options && options.parseFragmentIdentifier && hash ? {fragmentIdentifier: decode(hash, options)} : {}
  345. );
  346. };
  347. exports.stringifyUrl = (object, options) => {
  348. options = Object.assign({
  349. encode: true,
  350. strict: true,
  351. [encodeFragmentIdentifier]: true
  352. }, options);
  353. const url = removeHash(object.url).split('?')[0] || '';
  354. const queryFromUrl = exports.extract(object.url);
  355. const parsedQueryFromUrl = exports.parse(queryFromUrl, {sort: false});
  356. const query = Object.assign(parsedQueryFromUrl, object.query);
  357. let queryString = exports.stringify(query, options);
  358. if (queryString) {
  359. queryString = `?${queryString}`;
  360. }
  361. let hash = getHash(object.url);
  362. if (object.fragmentIdentifier) {
  363. hash = `#${options[encodeFragmentIdentifier] ? encode(object.fragmentIdentifier, options) : object.fragmentIdentifier}`;
  364. }
  365. return `${url}${queryString}${hash}`;
  366. };
  367. exports.pick = (input, filter, options) => {
  368. options = Object.assign({
  369. parseFragmentIdentifier: true,
  370. [encodeFragmentIdentifier]: false
  371. }, options);
  372. const {url, query, fragmentIdentifier} = exports.parseUrl(input, options);
  373. return exports.stringifyUrl({
  374. url,
  375. query: filterObject(query, filter),
  376. fragmentIdentifier
  377. }, options);
  378. };
  379. exports.exclude = (input, filter, options) => {
  380. const exclusionFilter = Array.isArray(filter) ? key => !filter.includes(key) : (key, value) => !filter(key, value);
  381. return exports.pick(input, exclusionFilter, options);
  382. };