domurl
Version:
Lightweight URL manipulation with JavaScript.
448 lines (375 loc) • 12.3 kB
JavaScript
/*!
* Lightweight URL manipulation with JavaScript
* This library is independent of any other libraries and has pretty simple
* interface and lightweight code-base.
* Some ideas of query string parsing had been taken from Jan Wolter
* @see http://unixpapa.com/js/querystring.html
*
* @license MIT
* @author Mykhailo Stadnyk <mikhus@gmail.com> and contributors
* @see https://github.com/Mikhus/domurl/graphs/contributors
*/
(function (ns) {
'use strict';
var RX_PROTOCOL = /^[a-z]+:/;
var RX_PORT = /[-a-z0-9]+(\.[-a-z0-9])*:\d+/i;
var RX_CREDS = /\/\/(.*?)(?::(.*?))?@/;
var RX_WIN = /^win/i;
var RX_PROTOCOL_REPL = /:$/;
var RX_QUERY_REPL = /^\?/;
var RX_HASH_REPL = /^#/;
var RX_PATH = /(.*\/)/;
var RX_PATH_FIX = /^\/{2,}/;
// https://news.ycombinator.com/item?id=3939454
var RX_PATH_IE_FIX = /(^\/?)/;
var RX_SINGLE_QUOTE = /'/g;
var RX_DECODE_1 = /%([ef][0-9a-f])%([89ab][0-9a-f])%([89ab][0-9a-f])/gi;
var RX_DECODE_2 = /%([cd][0-9a-f])%([89ab][0-9a-f])/gi;
var RX_DECODE_3 = /%([0-7][0-9a-f])/gi;
var RX_PLUS = /\+/g;
var RX_PATH_SEMI = /^\w:$/;
var RX_URL_TEST = /[^/#?]/;
// configure given url options
function urlConfig (url) {
var config = {
path: true,
query: true,
hash: true
};
if (!url) {
return config;
}
if (RX_PROTOCOL.test(url)) {
config.protocol = true;
config.host = true;
if (RX_PORT.test(url)) {
config.port = true;
}
if (RX_CREDS.test(url)) {
config.user = true;
config.pass = true;
}
}
return config;
}
var isNode = typeof window === 'undefined' &&
typeof global !== 'undefined' &&
typeof require === 'function';
var isIe = !isNode && ns.navigator && ns.navigator.userAgent &&
~ns.navigator.userAgent.indexOf('MSIE');
// Trick to bypass Webpack's require at compile time
var nodeRequire = isNode ? ns['require'] : null;
// mapping between what we want and <a> element properties
var map = {
protocol: 'protocol',
host: 'hostname',
port: 'port',
path: 'pathname',
query: 'search',
hash: 'hash'
};
// jscs: disable
/**
* default ports as defined by http://url.spec.whatwg.org/#default-port
* We need them to fix IE behavior,
* @see https://github.com/Mikhus/jsurl/issues/2
*/
// jscs: enable
var defaultPorts = {
ftp: 21,
gopher: 70,
http: 80,
https: 443,
ws: 80,
wss: 443
};
var _currNodeUrl;
function getCurrUrl() {
if (isNode) {
if (!_currNodeUrl) {
_currNodeUrl = ('file://' +
(process.platform.match(RX_WIN) ? '/' : '') +
nodeRequire('fs').realpathSync('.')
);
}
return _currNodeUrl;
} else if (document.location.href === 'about:srcdoc') {
return self.parent.document.location.href;
} else {
return document.location.href;
}
}
function parse (self, url, absolutize) {
var link, i, auth;
if (!url) {
url = getCurrUrl();
}
if (isNode) {
link = nodeRequire('url').parse(url);
}
else {
link = document.createElement('a');
link.href = url;
}
var config = urlConfig(url);
auth = url.match(RX_CREDS) || [];
for (i in map) {
if (config[i]) {
self[i] = link[map[i]] || '';
} else {
self[i] = '';
}
}
// fix-up some parts
self.protocol = self.protocol.replace(RX_PROTOCOL_REPL, '');
self.query = self.query.replace(RX_QUERY_REPL, '');
self.hash = decode(self.hash.replace(RX_HASH_REPL, ''));
self.user = decode(auth[1] || '');
self.pass = decode(auth[2] || '');
/* jshint ignore:start */
self.port = (
// loosely compare because port can be a string
defaultPorts[self.protocol] == self.port || self.port == 0
) ? '' : self.port; // IE fix, Android browser fix
/* jshint ignore:end */
if (!config.protocol && RX_URL_TEST.test(url.charAt(0))) {
self.path = url.split('?')[0].split('#')[0];
}
if (!config.protocol && absolutize) {
// is IE and path is relative
var base = new Url(getCurrUrl().match(RX_PATH)[0]);
var basePath = base.path.split('/');
var selfPath = self.path.split('/');
var props = ['protocol', 'user', 'pass', 'host', 'port'];
var s = props.length;
basePath.pop();
for (i = 0; i < s; i++) {
self[props[i]] = base[props[i]];
}
while (selfPath[0] === '..') { // skip all "../
basePath.pop();
selfPath.shift();
}
self.path =
(url.charAt(0) !== '/' ? basePath.join('/') : '') +
'/' + selfPath.join('/')
;
}
self.path = self.path.replace(RX_PATH_FIX, '/');
isIe && (self.path = self.path.replace(RX_PATH_IE_FIX, '/'));
self.paths(self.paths());
self.query = new QueryString(self.query);
}
function encode (s) {
return encodeURIComponent(s).replace(RX_SINGLE_QUOTE, '%27');
}
function decode (s) {
s = s.replace(RX_PLUS, ' ');
s = s.replace(RX_DECODE_1, function (code, hex1, hex2, hex3) {
var n1 = parseInt(hex1, 16) - 0xE0;
var n2 = parseInt(hex2, 16) - 0x80;
if (n1 === 0 && n2 < 32) {
return code;
}
var n3 = parseInt(hex3, 16) - 0x80;
var n = (n1 << 12) + (n2 << 6) + n3;
if (n > 0xFFFF) {
return code;
}
return String.fromCharCode(n);
});
s = s.replace(RX_DECODE_2, function (code, hex1, hex2) {
var n1 = parseInt(hex1, 16) - 0xC0;
if (n1 < 2) {
return code;
}
var n2 = parseInt(hex2, 16) - 0x80;
return String.fromCharCode((n1 << 6) + n2);
});
return s.replace(RX_DECODE_3, function (code, hex) {
return String.fromCharCode(parseInt(hex, 16));
});
}
/**
* Class QueryString
*
* @param {string} qs - string representation of QueryString
* @constructor
*/
function QueryString (qs) {
var parts = qs.split('&');
for (var i = 0, s = parts.length; i < s; i++) {
var keyVal = parts[i].split('=');
var key = decodeURIComponent(keyVal[0].replace(RX_PLUS, ' '));
if (!key) {
continue;
}
var value = keyVal[1] !== undefined ? decode(keyVal[1]) : null;
if (this[key] === undefined) {
this[key] = value;
} else {
if (!(this[key] instanceof Array)) {
this[key] = [this[key]];
}
this[key].push(value);
}
}
}
/**
* Converts QueryString object back to string representation
*
* @returns {string}
*/
QueryString.prototype.toString = function () {
var s = '';
var e = encode;
var i, ii;
for (i in this) {
var w = this[i];
if (w instanceof Function || w === undefined) {
continue;
}
if (w instanceof Array) {
var len = w.length;
if (!len) {
// Parameter is an empty array, so treat as
// an empty argument
s += (s ? '&' : '') + e(i) + '=';
continue;
}
for (ii = 0; ii < len; ii++) {
var v = w[ii];
if (v === undefined) {
continue;
}
s += s ? '&' : '';
s += e(i) + (v === null
? ''
: '=' + e(v));
}
continue;
}
// Plain value
s += s ? '&' : '';
s += e(i) + (w === null ? '' : '=' + e(w));
}
return s;
};
/**
* Class Url
*
* @param {string} [url] - string URL representation
* @param {boolean} [noTransform] - do not transform to absolute URL
* @constructor
*/
function Url (url, noTransform) {
parse(this, url, !noTransform);
}
/**
* Clears QueryString, making it contain no params at all
*
* @returns {Url}
*/
Url.prototype.clearQuery = function () {
for (var key in this.query) {
if (!(this.query[key] instanceof Function)) {
delete this.query[key];
}
}
return this;
};
/**
* Returns total number of parameters in QueryString
*
* @returns {number}
*/
Url.prototype.queryLength = function () {
var count = 0;
for (var key in this.query) {
if (!(this.query[key] instanceof Function)) {
count++;
}
}
return count;
};
/**
* Returns true if QueryString contains no parameters, false otherwise
*
* @returns {boolean}
*/
Url.prototype.isEmptyQuery = function () {
return this.queryLength() === 0;
};
/**
*
* @param {Array} [paths] - an array pf path parts (if given will modify
* Url.path property
* @returns {Array} - an array representation of the Url.path property
*/
Url.prototype.paths = function (paths) {
var prefix = '';
var i = 0;
var s;
if (paths && paths.length && paths + '' !== paths) {
if (this.isAbsolute()) {
prefix = '/';
}
for (s = paths.length; i < s; i++) {
paths[i] = !i && RX_PATH_SEMI.test(paths[i])
? paths[i]
: encode(paths[i]);
}
this.path = prefix + paths.join('/');
}
paths = (this.path.charAt(0) === '/' ?
this.path.slice(1) : this.path).split('/');
for (i = 0, s = paths.length; i < s; i++) {
paths[i] = decode(paths[i]);
}
return paths;
};
/**
* Performs URL-specific encoding of the given string
*
* @method Url#encode
* @param {string} s - string to encode
* @returns {string}
*/
Url.prototype.encode = encode;
/**
* Performs URL-specific decoding of the given encoded string
*
* @method Url#decode
* @param {string} s - string to decode
* @returns {string}
*/
Url.prototype.decode = decode;
/**
* Checks if current URL is an absolute resource locator (globally absolute
* or absolute path to current server)
*
* @returns {boolean}
*/
Url.prototype.isAbsolute = function () {
return this.protocol || this.path.charAt(0) === '/';
};
/**
* Returns string representation of current Url object
*
* @returns {string}
*/
Url.prototype.toString = function () {
return (
(this.protocol && (this.protocol + '://')) +
(this.user && (
encode(this.user) + (this.pass && (':' + encode(this.pass))
) + '@')) +
(this.host && this.host) +
(this.port && (':' + this.port)) +
(this.path && this.path) +
(this.query.toString() && ('?' + this.query)) +
(this.hash && ('#' + encode(this.hash)))
);
};
ns[ns.exports ? 'exports' : 'Url'] = Url;
}(typeof module !== 'undefined' && module.exports ? module : window));