url.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. // Copyright 2013-2014 Kevin Cox
  2. /*******************************************************************************
  3. * *
  4. * This software is provided 'as-is', without any express or implied *
  5. * warranty. In no event will the authors be held liable for any damages *
  6. * arising from the use of this software. *
  7. * *
  8. * Permission is granted to anyone to use this software for any purpose, *
  9. * including commercial applications, and to alter it and redistribute it *
  10. * freely, subject to the following restrictions: *
  11. * *
  12. * 1. The origin of this software must not be misrepresented; you must not *
  13. * claim that you wrote the original software. If you use this software in *
  14. * a product, an acknowledgment in the product documentation would be *
  15. * appreciated but is not required. *
  16. * *
  17. * 2. Altered source versions must be plainly marked as such, and must not be *
  18. * misrepresented as being the original software. *
  19. * *
  20. * 3. This notice may not be removed or altered from any source distribution. *
  21. * *
  22. *******************************************************************************/
  23. +function(){
  24. "use strict";
  25. var array = /\[([^\[]*)\]$/;
  26. /// URL Regex.
  27. /**
  28. * This regex splits the URL into parts. The capture groups catch the important
  29. * bits.
  30. *
  31. * Each section is optional, so to work on any part find the correct top level
  32. * `(...)?` and mess around with it.
  33. */
  34. var regex = /^(?:([a-z]*):)?(?:\/\/)?(?:([^:@]*)(?::([^@]*))?@)?([0-9a-z-._]+)?(?::([0-9]*))?(\/[^?#]*)?(?:\?([^#]*))?(?:#(.*))?$/i;
  35. // 1 - scheme 2 - user 3 = pass 4 - host 5 - port 6 - path 7 - query 8 - hash
  36. var noslash = ["mailto","bitcoin"];
  37. var self = {
  38. /** Parse a query string.
  39. *
  40. * This function parses a query string (sometimes called the search
  41. * string). It takes a query string and returns a map of the results.
  42. *
  43. * Keys are considered to be everything up to the first '=' and values are
  44. * everything afterwords. Since URL-decoding is done after parsing, keys
  45. * and values can have any values, however, '=' have to be encoded in keys
  46. * while '?' and '&' have to be encoded anywhere (as they delimit the
  47. * kv-pairs).
  48. *
  49. * Keys and values will always be strings, except if there is a key with no
  50. * '=' in which case it will be considered a flag and will be set to true.
  51. * Later values will override earlier values.
  52. *
  53. * Array keys are also supported. By default keys in the form of `name[i]`
  54. * will be returned like that as strings. However, if you set the `array`
  55. * flag in the options object they will be parsed into arrays. Note that
  56. * although the object returned is an `Array` object all keys will be
  57. * written to it. This means that if you have a key such as `k[forEach]`
  58. * it will overwrite the `forEach` function on that array. Also note that
  59. * string properties always take precedence over array properties,
  60. * irrespective of where they are in the query string.
  61. *
  62. * url.get("array[1]=test&array[foo]=bar",{array:true}).array[1] === "test"
  63. * url.get("array[1]=test&array[foo]=bar",{array:true}).array.foo === "bar"
  64. * url.get("array=notanarray&array[0]=1",{array:true}).array === "notanarray"
  65. *
  66. * If array parsing is enabled keys in the form of `name[]` will
  67. * automatically be given the next available index. Note that this can be
  68. * overwritten with later values in the query string. For this reason is
  69. * is best not to mix the two formats, although it is safe (and often
  70. * useful) to add an automatic index argument to the end of a query string.
  71. *
  72. * url.get("a[]=0&a[]=1&a[0]=2", {array:true}) -> {a:["2","1"]};
  73. * url.get("a[0]=0&a[1]=1&a[]=2", {array:true}) -> {a:["0","1","2"]};
  74. *
  75. * @param{string} q The query string (the part after the '?').
  76. * @param{{full:boolean,array:boolean}=} opt Options.
  77. *
  78. * - full: If set `q` will be treated as a full url and `q` will be built.
  79. * by calling #parse to retrieve the query portion.
  80. * - array: If set keys in the form of `key[i]` will be treated
  81. * as arrays/maps.
  82. *
  83. * @return{!Object.<string, string|Array>} The parsed result.
  84. */
  85. "get": function(q, opt){
  86. q = q || "";
  87. if ( typeof opt == "undefined" ) opt = {};
  88. if ( typeof opt["full"] == "undefined" ) opt["full"] = false;
  89. if ( typeof opt["array"] == "undefined" ) opt["array"] = false;
  90. if ( opt["full"] === true )
  91. {
  92. q = self["parse"](q, {"get":false})["query"] || "";
  93. }
  94. var o = {};
  95. var c = q.split("&");
  96. for (var i = 0; i < c.length; i++)
  97. {
  98. if (!c[i].length) continue;
  99. var d = c[i].indexOf("=");
  100. var k = c[i], v = true;
  101. if ( d >= 0 )
  102. {
  103. k = c[i].substr(0, d);
  104. v = c[i].substr(d+1);
  105. v = decodeURIComponent(v);
  106. }
  107. if (opt["array"])
  108. {
  109. var inds = [];
  110. var ind;
  111. var curo = o;
  112. var curk = k;
  113. while (ind = curk.match(array)) // Array!
  114. {
  115. curk = curk.substr(0, ind.index);
  116. inds.unshift(decodeURIComponent(ind[1]));
  117. }
  118. curk = decodeURIComponent(curk);
  119. if (inds.some(function(i)
  120. {
  121. if ( typeof curo[curk] == "undefined" ) curo[curk] = [];
  122. if (!Array.isArray(curo[curk]))
  123. {
  124. //console.log("url.get: Array property "+curk+" already exists as string!");
  125. return true;
  126. }
  127. curo = curo[curk];
  128. if ( i === "" ) i = curo.length;
  129. curk = i;
  130. })) continue;
  131. curo[curk] = v;
  132. continue;
  133. }
  134. k = decodeURIComponent(k);
  135. //typeof o[k] == "undefined" || console.log("Property "+k+" already exists!");
  136. o[k] = v;
  137. }
  138. return o;
  139. },
  140. /** Build a get query from an object.
  141. *
  142. * This constructs a query string from the kv pairs in `data`. Calling
  143. * #get on the string returned should return an object identical to the one
  144. * passed in except all non-boolean scalar types become strings and all
  145. * object types become arrays (non-integer keys are still present, see
  146. * #get's documentation for more details).
  147. *
  148. * This always uses array syntax for describing arrays. If you want to
  149. * serialize them differently (like having the value be a JSON array and
  150. * have a plain key) you will need to do that before passing it in.
  151. *
  152. * All keys and values are supported (binary data anyone?) as they are
  153. * properly URL-encoded and #get properly decodes.
  154. *
  155. * @param{Object} data The kv pairs.
  156. * @param{string} prefix The properly encoded array key to put the
  157. * properties. Mainly intended for internal use.
  158. * @return{string} A URL-safe string.
  159. */
  160. "buildget": function(data, prefix){
  161. var itms = [];
  162. for ( var k in data )
  163. {
  164. var ek = encodeURIComponent(k);
  165. if ( typeof prefix != "undefined" )
  166. ek = prefix+"["+ek+"]";
  167. var v = data[k];
  168. switch (typeof v)
  169. {
  170. case 'boolean':
  171. if(v) itms.push(ek);
  172. break;
  173. case 'number':
  174. v = v.toString();
  175. case 'string':
  176. itms.push(ek+"="+encodeURIComponent(v));
  177. break;
  178. case 'object':
  179. itms.push(self["buildget"](v, ek));
  180. break;
  181. }
  182. }
  183. return itms.join("&");
  184. },
  185. /** Parse a URL
  186. *
  187. * This breaks up a URL into components. It attempts to be very liberal
  188. * and returns the best result in most cases. This means that you can
  189. * often pass in part of a URL and get correct categories back. Notably,
  190. * this works for emails and Jabber IDs, as well as adding a '?' to the
  191. * beginning of a string will parse the whole thing as a query string. If
  192. * an item is not found the property will be undefined. In some cases an
  193. * empty string will be returned if the surrounding syntax but the actual
  194. * value is empty (example: "://example.com" will give a empty string for
  195. * scheme.) Notably the host name will always be set to something.
  196. *
  197. * Returned properties.
  198. *
  199. * - **scheme:** The url scheme. (ex: "mailto" or "https")
  200. * - **user:** The username.
  201. * - **pass:** The password.
  202. * - **host:** The hostname. (ex: "localhost", "123.456.7.8" or "example.com")
  203. * - **port:** The port, as a number. (ex: 1337)
  204. * - **path:** The path. (ex: "/" or "/about.html")
  205. * - **query:** "The query string. (ex: "foo=bar&v=17&format=json")
  206. * - **get:** The query string parsed with get. If `opt.get` is `false` this
  207. * will be absent
  208. * - **hash:** The value after the hash. (ex: "myanchor")
  209. * be undefined even if `query` is set.
  210. *
  211. * @param{string} url The URL to parse.
  212. * @param{{get:Object}=} opt Options:
  213. *
  214. * - get: An options argument to be passed to #get or false to not call #get.
  215. * **DO NOT** set `full`.
  216. *
  217. * @return{!Object} An object with the parsed values.
  218. */
  219. "parse": function(url, opt) {
  220. if ( typeof opt == "undefined" ) opt = {};
  221. var md = url.match(regex) || [];
  222. var r = {
  223. "url": url,
  224. "scheme": md[1],
  225. "user": md[2],
  226. "pass": md[3],
  227. "host": md[4],
  228. "port": md[5] && +md[5],
  229. "path": md[6],
  230. "query": md[7],
  231. "hash": md[8],
  232. };
  233. if ( opt.get !== false )
  234. r["get"] = r["query"] && self["get"](r["query"], opt.get);
  235. return r;
  236. },
  237. /** Build a URL from components.
  238. *
  239. * This pieces together a url from the properties of the passed in object.
  240. * In general passing the result of `parse()` should return the URL. There
  241. * may differences in the get string as the keys and values might be more
  242. * encoded then they were originally were. However, calling `get()` on the
  243. * two values should yield the same result.
  244. *
  245. * Here is how the parameters are used.
  246. *
  247. * - url: Used only if no other values are provided. If that is the case
  248. * `url` will be returned verbatim.
  249. * - scheme: Used if defined.
  250. * - user: Used if defined.
  251. * - pass: Used if defined.
  252. * - host: Used if defined.
  253. * - path: Used if defined.
  254. * - query: Used only if `get` is not provided and non-empty.
  255. * - get: Used if non-empty. Passed to #buildget and the result is used
  256. * as the query string.
  257. * - hash: Used if defined.
  258. *
  259. * These are the options that are valid on the options object.
  260. *
  261. * - useemptyget: If truthy, a question mark will be appended for empty get
  262. * strings. This notably makes `build()` and `parse()` fully symmetric.
  263. *
  264. * @param{Object} data The pieces of the URL.
  265. * @param{Object} opt Options for building the url.
  266. * @return{string} The URL.
  267. */
  268. "build": function(data, opt){
  269. opt = opt || {};
  270. var r = "";
  271. if ( typeof data["scheme"] != "undefined" )
  272. {
  273. r += data["scheme"];
  274. r += (noslash.indexOf(data["scheme"])>=0)?":":"://";
  275. }
  276. if ( typeof data["user"] != "undefined" )
  277. {
  278. r += data["user"];
  279. if ( typeof data["pass"] == "undefined" )
  280. {
  281. r += "@";
  282. }
  283. }
  284. if ( typeof data["pass"] != "undefined" ) r += ":" + data["pass"] + "@";
  285. if ( typeof data["host"] != "undefined" ) r += data["host"];
  286. if ( typeof data["port"] != "undefined" ) r += ":" + data["port"];
  287. if ( typeof data["path"] != "undefined" ) r += data["path"];
  288. if (opt["useemptyget"])
  289. {
  290. if ( typeof data["get"] != "undefined" ) r += "?" + self["buildget"](data["get"]);
  291. else if ( typeof data["query"] != "undefined" ) r += "?" + data["query"];
  292. }
  293. else
  294. {
  295. // If .get use it. If .get leads to empty, use .query.
  296. var q = data["get"] && self["buildget"](data["get"]) || data["query"];
  297. if (q) r += "?" + q;
  298. }
  299. if ( typeof data["hash"] != "undefined" ) r += "#" + data["hash"];
  300. return r || data["url"] || "";
  301. },
  302. };
  303. if ( typeof define != "undefined" && define["amd"] ) define(self);
  304. else if ( typeof module != "undefined" ) module['exports'] = self;
  305. else window["url"] = self;
  306. }();