index.js 9.3 KB

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