index.js 14 KB

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