index.js 11 KB

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