| 'use strict'; |
| |
| var required = require('requires-port') |
| , qs = require('querystringify') |
| , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\// |
| , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i |
| , 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]' |
| , left = new RegExp('^'+ whitespace +'+'); |
| |
| /** |
| * Trim a given string. |
| * |
| * @param {String} str String to trim. |
| * @public |
| */ |
| function trimLeft(str) { |
| return (str ? str : '').toString().replace(left, ''); |
| } |
| |
| /** |
| * These are the parse rules for the URL parser, it informs the parser |
| * about: |
| * |
| * 0. The char it Needs to parse, if it's a string it should be done using |
| * indexOf, RegExp using exec and NaN means set as current value. |
| * 1. The property we should set when parsing this value. |
| * 2. Indication if it's backwards or forward parsing, when set as number it's |
| * the value of extra chars that should be split off. |
| * 3. Inherit from location if non existing in the parser. |
| * 4. `toLowerCase` the resulting value. |
| */ |
| var rules = [ |
| ['#', 'hash'], // Extract from the back. |
| ['?', 'query'], // Extract from the back. |
| function sanitize(address) { // Sanitize what is left of the address |
| return address.replace('\\', '/'); |
| }, |
| ['/', 'pathname'], // Extract from the back. |
| ['@', 'auth', 1], // Extract from the front. |
| [NaN, 'host', undefined, 1, 1], // Set left over value. |
| [/:(\d+)$/, 'port', undefined, 1], // RegExp the back. |
| [NaN, 'hostname', undefined, 1, 1] // Set left over. |
| ]; |
| |
| /** |
| * These properties should not be copied or inherited from. This is only needed |
| * for all non blob URL's as a blob URL does not include a hash, only the |
| * origin. |
| * |
| * @type {Object} |
| * @private |
| */ |
| var ignore = { hash: 1, query: 1 }; |
| |
| /** |
| * The location object differs when your code is loaded through a normal page, |
| * Worker or through a worker using a blob. And with the blobble begins the |
| * trouble as the location object will contain the URL of the blob, not the |
| * location of the page where our code is loaded in. The actual origin is |
| * encoded in the `pathname` so we can thankfully generate a good "default" |
| * location from it so we can generate proper relative URL's again. |
| * |
| * @param {Object|String} loc Optional default location object. |
| * @returns {Object} lolcation object. |
| * @public |
| */ |
| function lolcation(loc) { |
| var globalVar; |
| |
| if (typeof window !== 'undefined') globalVar = window; |
| else if (typeof global !== 'undefined') globalVar = global; |
| else if (typeof self !== 'undefined') globalVar = self; |
| else globalVar = {}; |
| |
| var location = globalVar.location || {}; |
| loc = loc || location; |
| |
| var finaldestination = {} |
| , type = typeof loc |
| , key; |
| |
| if ('blob:' === loc.protocol) { |
| finaldestination = new Url(unescape(loc.pathname), {}); |
| } else if ('string' === type) { |
| finaldestination = new Url(loc, {}); |
| for (key in ignore) delete finaldestination[key]; |
| } else if ('object' === type) { |
| for (key in loc) { |
| if (key in ignore) continue; |
| finaldestination[key] = loc[key]; |
| } |
| |
| if (finaldestination.slashes === undefined) { |
| finaldestination.slashes = slashes.test(loc.href); |
| } |
| } |
| |
| return finaldestination; |
| } |
| |
| /** |
| * @typedef ProtocolExtract |
| * @type Object |
| * @property {String} protocol Protocol matched in the URL, in lowercase. |
| * @property {Boolean} slashes `true` if protocol is followed by "//", else `false`. |
| * @property {String} rest Rest of the URL that is not part of the protocol. |
| */ |
| |
| /** |
| * Extract protocol information from a URL with/without double slash ("//"). |
| * |
| * @param {String} address URL we want to extract from. |
| * @return {ProtocolExtract} Extracted information. |
| * @private |
| */ |
| function extractProtocol(address) { |
| address = trimLeft(address); |
| var match = protocolre.exec(address); |
| |
| return { |
| protocol: match[1] ? match[1].toLowerCase() : '', |
| slashes: !!match[2], |
| rest: match[3] |
| }; |
| } |
| |
| /** |
| * Resolve a relative URL pathname against a base URL pathname. |
| * |
| * @param {String} relative Pathname of the relative URL. |
| * @param {String} base Pathname of the base URL. |
| * @return {String} Resolved pathname. |
| * @private |
| */ |
| function resolve(relative, base) { |
| if (relative === '') return base; |
| |
| var path = (base || '/').split('/').slice(0, -1).concat(relative.split('/')) |
| , i = path.length |
| , last = path[i - 1] |
| , unshift = false |
| , up = 0; |
| |
| while (i--) { |
| if (path[i] === '.') { |
| path.splice(i, 1); |
| } else if (path[i] === '..') { |
| path.splice(i, 1); |
| up++; |
| } else if (up) { |
| if (i === 0) unshift = true; |
| path.splice(i, 1); |
| up--; |
| } |
| } |
| |
| if (unshift) path.unshift(''); |
| if (last === '.' || last === '..') path.push(''); |
| |
| return path.join('/'); |
| } |
| |
| /** |
| * The actual URL instance. Instead of returning an object we've opted-in to |
| * create an actual constructor as it's much more memory efficient and |
| * faster and it pleases my OCD. |
| * |
| * It is worth noting that we should not use `URL` as class name to prevent |
| * clashes with the global URL instance that got introduced in browsers. |
| * |
| * @constructor |
| * @param {String} address URL we want to parse. |
| * @param {Object|String} [location] Location defaults for relative paths. |
| * @param {Boolean|Function} [parser] Parser for the query string. |
| * @private |
| */ |
| function Url(address, location, parser) { |
| address = trimLeft(address); |
| |
| if (!(this instanceof Url)) { |
| return new Url(address, location, parser); |
| } |
| |
| var relative, extracted, parse, instruction, index, key |
| , instructions = rules.slice() |
| , type = typeof location |
| , url = this |
| , i = 0; |
| |
| // |
| // The following if statements allows this module two have compatibility with |
| // 2 different API: |
| // |
| // 1. Node.js's `url.parse` api which accepts a URL, boolean as arguments |
| // where the boolean indicates that the query string should also be parsed. |
| // |
| // 2. The `URL` interface of the browser which accepts a URL, object as |
| // arguments. The supplied object will be used as default values / fall-back |
| // for relative paths. |
| // |
| if ('object' !== type && 'string' !== type) { |
| parser = location; |
| location = null; |
| } |
| |
| if (parser && 'function' !== typeof parser) parser = qs.parse; |
| |
| location = lolcation(location); |
| |
| // |
| // Extract protocol information before running the instructions. |
| // |
| extracted = extractProtocol(address || ''); |
| relative = !extracted.protocol && !extracted.slashes; |
| url.slashes = extracted.slashes || relative && location.slashes; |
| url.protocol = extracted.protocol || location.protocol || ''; |
| address = extracted.rest; |
| |
| // |
| // When the authority component is absent the URL starts with a path |
| // component. |
| // |
| if (!extracted.slashes) instructions[3] = [/(.*)/, 'pathname']; |
| |
| for (; i < instructions.length; i++) { |
| instruction = instructions[i]; |
| |
| if (typeof instruction === 'function') { |
| address = instruction(address); |
| continue; |
| } |
| |
| parse = instruction[0]; |
| key = instruction[1]; |
| |
| if (parse !== parse) { |
| url[key] = address; |
| } else if ('string' === typeof parse) { |
| if (~(index = address.indexOf(parse))) { |
| if ('number' === typeof instruction[2]) { |
| url[key] = address.slice(0, index); |
| address = address.slice(index + instruction[2]); |
| } else { |
| url[key] = address.slice(index); |
| address = address.slice(0, index); |
| } |
| } |
| } else if ((index = parse.exec(address))) { |
| url[key] = index[1]; |
| address = address.slice(0, index.index); |
| } |
| |
| url[key] = url[key] || ( |
| relative && instruction[3] ? location[key] || '' : '' |
| ); |
| |
| // |
| // Hostname, host and protocol should be lowercased so they can be used to |
| // create a proper `origin`. |
| // |
| if (instruction[4]) url[key] = url[key].toLowerCase(); |
| } |
| |
| // |
| // Also parse the supplied query string in to an object. If we're supplied |
| // with a custom parser as function use that instead of the default build-in |
| // parser. |
| // |
| if (parser) url.query = parser(url.query); |
| |
| // |
| // If the URL is relative, resolve the pathname against the base URL. |
| // |
| if ( |
| relative |
| && location.slashes |
| && url.pathname.charAt(0) !== '/' |
| && (url.pathname !== '' || location.pathname !== '') |
| ) { |
| url.pathname = resolve(url.pathname, location.pathname); |
| } |
| |
| // |
| // We should not add port numbers if they are already the default port number |
| // for a given protocol. As the host also contains the port number we're going |
| // override it with the hostname which contains no port number. |
| // |
| if (!required(url.port, url.protocol)) { |
| url.host = url.hostname; |
| url.port = ''; |
| } |
| |
| // |
| // Parse down the `auth` for the username and password. |
| // |
| url.username = url.password = ''; |
| if (url.auth) { |
| instruction = url.auth.split(':'); |
| url.username = instruction[0] || ''; |
| url.password = instruction[1] || ''; |
| } |
| |
| url.origin = url.protocol && url.host && url.protocol !== 'file:' |
| ? url.protocol +'//'+ url.host |
| : 'null'; |
| |
| // |
| // The href is just the compiled result. |
| // |
| url.href = url.toString(); |
| } |
| |
| /** |
| * This is convenience method for changing properties in the URL instance to |
| * insure that they all propagate correctly. |
| * |
| * @param {String} part Property we need to adjust. |
| * @param {Mixed} value The newly assigned value. |
| * @param {Boolean|Function} fn When setting the query, it will be the function |
| * used to parse the query. |
| * When setting the protocol, double slash will be |
| * removed from the final url if it is true. |
| * @returns {URL} URL instance for chaining. |
| * @public |
| */ |
| function set(part, value, fn) { |
| var url = this; |
| |
| switch (part) { |
| case 'query': |
| if ('string' === typeof value && value.length) { |
| value = (fn || qs.parse)(value); |
| } |
| |
| url[part] = value; |
| break; |
| |
| case 'port': |
| url[part] = value; |
| |
| if (!required(value, url.protocol)) { |
| url.host = url.hostname; |
| url[part] = ''; |
| } else if (value) { |
| url.host = url.hostname +':'+ value; |
| } |
| |
| break; |
| |
| case 'hostname': |
| url[part] = value; |
| |
| if (url.port) value += ':'+ url.port; |
| url.host = value; |
| break; |
| |
| case 'host': |
| url[part] = value; |
| |
| if (/:\d+$/.test(value)) { |
| value = value.split(':'); |
| url.port = value.pop(); |
| url.hostname = value.join(':'); |
| } else { |
| url.hostname = value; |
| url.port = ''; |
| } |
| |
| break; |
| |
| case 'protocol': |
| url.protocol = value.toLowerCase(); |
| url.slashes = !fn; |
| break; |
| |
| case 'pathname': |
| case 'hash': |
| if (value) { |
| var char = part === 'pathname' ? '/' : '#'; |
| url[part] = value.charAt(0) !== char ? char + value : value; |
| } else { |
| url[part] = value; |
| } |
| break; |
| |
| default: |
| url[part] = value; |
| } |
| |
| for (var i = 0; i < rules.length; i++) { |
| var ins = rules[i]; |
| |
| if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase(); |
| } |
| |
| url.origin = url.protocol && url.host && url.protocol !== 'file:' |
| ? url.protocol +'//'+ url.host |
| : 'null'; |
| |
| url.href = url.toString(); |
| |
| return url; |
| } |
| |
| /** |
| * Transform the properties back in to a valid and full URL string. |
| * |
| * @param {Function} stringify Optional query stringify function. |
| * @returns {String} Compiled version of the URL. |
| * @public |
| */ |
| function toString(stringify) { |
| if (!stringify || 'function' !== typeof stringify) stringify = qs.stringify; |
| |
| var query |
| , url = this |
| , protocol = url.protocol; |
| |
| if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':'; |
| |
| var result = protocol + (url.slashes ? '//' : ''); |
| |
| if (url.username) { |
| result += url.username; |
| if (url.password) result += ':'+ url.password; |
| result += '@'; |
| } |
| |
| result += url.host + url.pathname; |
| |
| query = 'object' === typeof url.query ? stringify(url.query) : url.query; |
| if (query) result += '?' !== query.charAt(0) ? '?'+ query : query; |
| |
| if (url.hash) result += url.hash; |
| |
| return result; |
| } |
| |
| Url.prototype = { set: set, toString: toString }; |
| |
| // |
| // Expose the URL parser and some additional properties that might be useful for |
| // others or testing. |
| // |
| Url.extractProtocol = extractProtocol; |
| Url.location = lolcation; |
| Url.trimLeft = trimLeft; |
| Url.qs = qs; |
| |
| module.exports = Url; |