index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. 'use strict';
  2. var required = require('requires-port')
  3. , qs = require('querystringify')
  4. , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i
  5. , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//;
  6. /**
  7. * These are the parse rules for the URL parser, it informs the parser
  8. * about:
  9. *
  10. * 0. The char it Needs to parse, if it's a string it should be done using
  11. * indexOf, RegExp using exec and NaN means set as current value.
  12. * 1. The property we should set when parsing this value.
  13. * 2. Indication if it's backwards or forward parsing, when set as number it's
  14. * the value of extra chars that should be split off.
  15. * 3. Inherit from location if non existing in the parser.
  16. * 4. `toLowerCase` the resulting value.
  17. */
  18. var rules = [
  19. ['#', 'hash'], // Extract from the back.
  20. ['?', 'query'], // Extract from the back.
  21. function sanitize(address) { // Sanitize what is left of the address
  22. return address.replace('\\', '/');
  23. },
  24. ['/', 'pathname'], // Extract from the back.
  25. ['@', 'auth', 1], // Extract from the front.
  26. [NaN, 'host', undefined, 1, 1], // Set left over value.
  27. [/:(\d+)$/, 'port', undefined, 1], // RegExp the back.
  28. [NaN, 'hostname', undefined, 1, 1] // Set left over.
  29. ];
  30. /**
  31. * These properties should not be copied or inherited from. This is only needed
  32. * for all non blob URL's as a blob URL does not include a hash, only the
  33. * origin.
  34. *
  35. * @type {Object}
  36. * @private
  37. */
  38. var ignore = { hash: 1, query: 1 };
  39. /**
  40. * The location object differs when your code is loaded through a normal page,
  41. * Worker or through a worker using a blob. And with the blobble begins the
  42. * trouble as the location object will contain the URL of the blob, not the
  43. * location of the page where our code is loaded in. The actual origin is
  44. * encoded in the `pathname` so we can thankfully generate a good "default"
  45. * location from it so we can generate proper relative URL's again.
  46. *
  47. * @param {Object|String} loc Optional default location object.
  48. * @returns {Object} lolcation object.
  49. * @public
  50. */
  51. function lolcation(loc) {
  52. var globalVar;
  53. if (typeof window !== 'undefined') globalVar = window;
  54. else if (typeof global !== 'undefined') globalVar = global;
  55. else if (typeof self !== 'undefined') globalVar = self;
  56. else globalVar = {};
  57. var location = globalVar.location || {};
  58. loc = loc || location;
  59. var finaldestination = {}
  60. , type = typeof loc
  61. , key;
  62. if ('blob:' === loc.protocol) {
  63. finaldestination = new Url(unescape(loc.pathname), {});
  64. } else if ('string' === type) {
  65. finaldestination = new Url(loc, {});
  66. for (key in ignore) delete finaldestination[key];
  67. } else if ('object' === type) {
  68. for (key in loc) {
  69. if (key in ignore) continue;
  70. finaldestination[key] = loc[key];
  71. }
  72. if (finaldestination.slashes === undefined) {
  73. finaldestination.slashes = slashes.test(loc.href);
  74. }
  75. }
  76. return finaldestination;
  77. }
  78. /**
  79. * @typedef ProtocolExtract
  80. * @type Object
  81. * @property {String} protocol Protocol matched in the URL, in lowercase.
  82. * @property {Boolean} slashes `true` if protocol is followed by "//", else `false`.
  83. * @property {String} rest Rest of the URL that is not part of the protocol.
  84. */
  85. /**
  86. * Extract protocol information from a URL with/without double slash ("//").
  87. *
  88. * @param {String} address URL we want to extract from.
  89. * @return {ProtocolExtract} Extracted information.
  90. * @private
  91. */
  92. function extractProtocol(address) {
  93. var match = protocolre.exec(address);
  94. return {
  95. protocol: match[1] ? match[1].toLowerCase() : '',
  96. slashes: !!match[2],
  97. rest: match[3]
  98. };
  99. }
  100. /**
  101. * Resolve a relative URL pathname against a base URL pathname.
  102. *
  103. * @param {String} relative Pathname of the relative URL.
  104. * @param {String} base Pathname of the base URL.
  105. * @return {String} Resolved pathname.
  106. * @private
  107. */
  108. function resolve(relative, base) {
  109. var path = (base || '/').split('/').slice(0, -1).concat(relative.split('/'))
  110. , i = path.length
  111. , last = path[i - 1]
  112. , unshift = false
  113. , up = 0;
  114. while (i--) {
  115. if (path[i] === '.') {
  116. path.splice(i, 1);
  117. } else if (path[i] === '..') {
  118. path.splice(i, 1);
  119. up++;
  120. } else if (up) {
  121. if (i === 0) unshift = true;
  122. path.splice(i, 1);
  123. up--;
  124. }
  125. }
  126. if (unshift) path.unshift('');
  127. if (last === '.' || last === '..') path.push('');
  128. return path.join('/');
  129. }
  130. /**
  131. * The actual URL instance. Instead of returning an object we've opted-in to
  132. * create an actual constructor as it's much more memory efficient and
  133. * faster and it pleases my OCD.
  134. *
  135. * It is worth noting that we should not use `URL` as class name to prevent
  136. * clashes with the global URL instance that got introduced in browsers.
  137. *
  138. * @constructor
  139. * @param {String} address URL we want to parse.
  140. * @param {Object|String} [location] Location defaults for relative paths.
  141. * @param {Boolean|Function} [parser] Parser for the query string.
  142. * @private
  143. */
  144. function Url(address, location, parser) {
  145. if (!(this instanceof Url)) {
  146. return new Url(address, location, parser);
  147. }
  148. var relative, extracted, parse, instruction, index, key
  149. , instructions = rules.slice()
  150. , type = typeof location
  151. , url = this
  152. , i = 0;
  153. //
  154. // The following if statements allows this module two have compatibility with
  155. // 2 different API:
  156. //
  157. // 1. Node.js's `url.parse` api which accepts a URL, boolean as arguments
  158. // where the boolean indicates that the query string should also be parsed.
  159. //
  160. // 2. The `URL` interface of the browser which accepts a URL, object as
  161. // arguments. The supplied object will be used as default values / fall-back
  162. // for relative paths.
  163. //
  164. if ('object' !== type && 'string' !== type) {
  165. parser = location;
  166. location = null;
  167. }
  168. if (parser && 'function' !== typeof parser) parser = qs.parse;
  169. location = lolcation(location);
  170. //
  171. // Extract protocol information before running the instructions.
  172. //
  173. extracted = extractProtocol(address || '');
  174. relative = !extracted.protocol && !extracted.slashes;
  175. url.slashes = extracted.slashes || relative && location.slashes;
  176. url.protocol = extracted.protocol || location.protocol || '';
  177. address = extracted.rest;
  178. //
  179. // When the authority component is absent the URL starts with a path
  180. // component.
  181. //
  182. if (!extracted.slashes) instructions[3] = [/(.*)/, 'pathname'];
  183. for (; i < instructions.length; i++) {
  184. instruction = instructions[i];
  185. if (typeof instruction === 'function') {
  186. address = instruction(address);
  187. continue;
  188. }
  189. parse = instruction[0];
  190. key = instruction[1];
  191. if (parse !== parse) {
  192. url[key] = address;
  193. } else if ('string' === typeof parse) {
  194. if (~(index = address.indexOf(parse))) {
  195. if ('number' === typeof instruction[2]) {
  196. url[key] = address.slice(0, index);
  197. address = address.slice(index + instruction[2]);
  198. } else {
  199. url[key] = address.slice(index);
  200. address = address.slice(0, index);
  201. }
  202. }
  203. } else if ((index = parse.exec(address))) {
  204. url[key] = index[1];
  205. address = address.slice(0, index.index);
  206. }
  207. url[key] = url[key] || (
  208. relative && instruction[3] ? location[key] || '' : ''
  209. );
  210. //
  211. // Hostname, host and protocol should be lowercased so they can be used to
  212. // create a proper `origin`.
  213. //
  214. if (instruction[4]) url[key] = url[key].toLowerCase();
  215. }
  216. //
  217. // Also parse the supplied query string in to an object. If we're supplied
  218. // with a custom parser as function use that instead of the default build-in
  219. // parser.
  220. //
  221. if (parser) url.query = parser(url.query);
  222. //
  223. // If the URL is relative, resolve the pathname against the base URL.
  224. //
  225. if (
  226. relative
  227. && location.slashes
  228. && url.pathname.charAt(0) !== '/'
  229. && (url.pathname !== '' || location.pathname !== '')
  230. ) {
  231. url.pathname = resolve(url.pathname, location.pathname);
  232. }
  233. //
  234. // We should not add port numbers if they are already the default port number
  235. // for a given protocol. As the host also contains the port number we're going
  236. // override it with the hostname which contains no port number.
  237. //
  238. if (!required(url.port, url.protocol)) {
  239. url.host = url.hostname;
  240. url.port = '';
  241. }
  242. //
  243. // Parse down the `auth` for the username and password.
  244. //
  245. url.username = url.password = '';
  246. if (url.auth) {
  247. instruction = url.auth.split(':');
  248. url.username = instruction[0] || '';
  249. url.password = instruction[1] || '';
  250. }
  251. url.origin = url.protocol && url.host && url.protocol !== 'file:'
  252. ? url.protocol +'//'+ url.host
  253. : 'null';
  254. //
  255. // The href is just the compiled result.
  256. //
  257. url.href = url.toString();
  258. }
  259. /**
  260. * This is convenience method for changing properties in the URL instance to
  261. * insure that they all propagate correctly.
  262. *
  263. * @param {String} part Property we need to adjust.
  264. * @param {Mixed} value The newly assigned value.
  265. * @param {Boolean|Function} fn When setting the query, it will be the function
  266. * used to parse the query.
  267. * When setting the protocol, double slash will be
  268. * removed from the final url if it is true.
  269. * @returns {URL} URL instance for chaining.
  270. * @public
  271. */
  272. function set(part, value, fn) {
  273. var url = this;
  274. switch (part) {
  275. case 'query':
  276. if ('string' === typeof value && value.length) {
  277. value = (fn || qs.parse)(value);
  278. }
  279. url[part] = value;
  280. break;
  281. case 'port':
  282. url[part] = value;
  283. if (!required(value, url.protocol)) {
  284. url.host = url.hostname;
  285. url[part] = '';
  286. } else if (value) {
  287. url.host = url.hostname +':'+ value;
  288. }
  289. break;
  290. case 'hostname':
  291. url[part] = value;
  292. if (url.port) value += ':'+ url.port;
  293. url.host = value;
  294. break;
  295. case 'host':
  296. url[part] = value;
  297. if (/:\d+$/.test(value)) {
  298. value = value.split(':');
  299. url.port = value.pop();
  300. url.hostname = value.join(':');
  301. } else {
  302. url.hostname = value;
  303. url.port = '';
  304. }
  305. break;
  306. case 'protocol':
  307. url.protocol = value.toLowerCase();
  308. url.slashes = !fn;
  309. break;
  310. case 'pathname':
  311. case 'hash':
  312. if (value) {
  313. var char = part === 'pathname' ? '/' : '#';
  314. url[part] = value.charAt(0) !== char ? char + value : value;
  315. } else {
  316. url[part] = value;
  317. }
  318. break;
  319. default:
  320. url[part] = value;
  321. }
  322. for (var i = 0; i < rules.length; i++) {
  323. var ins = rules[i];
  324. if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase();
  325. }
  326. url.origin = url.protocol && url.host && url.protocol !== 'file:'
  327. ? url.protocol +'//'+ url.host
  328. : 'null';
  329. url.href = url.toString();
  330. return url;
  331. }
  332. /**
  333. * Transform the properties back in to a valid and full URL string.
  334. *
  335. * @param {Function} stringify Optional query stringify function.
  336. * @returns {String} Compiled version of the URL.
  337. * @public
  338. */
  339. function toString(stringify) {
  340. if (!stringify || 'function' !== typeof stringify) stringify = qs.stringify;
  341. var query
  342. , url = this
  343. , protocol = url.protocol;
  344. if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':';
  345. var result = protocol + (url.slashes ? '//' : '');
  346. if (url.username) {
  347. result += url.username;
  348. if (url.password) result += ':'+ url.password;
  349. result += '@';
  350. }
  351. result += url.host + url.pathname;
  352. query = 'object' === typeof url.query ? stringify(url.query) : url.query;
  353. if (query) result += '?' !== query.charAt(0) ? '?'+ query : query;
  354. if (url.hash) result += url.hash;
  355. return result;
  356. }
  357. Url.prototype = { set: set, toString: toString };
  358. //
  359. // Expose the URL parser and some additional properties that might be useful for
  360. // others or testing.
  361. //
  362. Url.extractProtocol = extractProtocol;
  363. Url.location = lolcation;
  364. Url.qs = qs;
  365. module.exports = Url;