diff --git a/README.md b/README.md index 0b92c41..f9888df 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ (jQuery) URL Parser v2.2 -====================== +======================== An AMD compatible utility to parse urls and provide easy access to their attributes (such as the protocol, host, port etc), path segments, querystring parameters, fragment parameters and more. @@ -10,7 +10,7 @@ The core parser functionality is based on the [Regex URI parser by Steven Levith **License:** Available for use under a MIT-style license. If you need a different license for any reason please just let me know. To jQuery or *not* to jQuery, that is the question... ----------------------------------------------------- +----------------------------------------------------- This utility can be used in two ways - with jQuery or without. There is just one file (purl.js) for both versions - if jQuery is included on the page before it then it will provide the 'jQuery-style' interface (see examples below), otherwise it will be accessible via the global `purl` variable. @@ -91,7 +91,7 @@ Note that the `.param()` method will work on both ampersand-split and semicolon- *As of version 2.2 the param method now handles array-style query string params.* URL segments ------------------------ +------------ The `.segment()` method is used to return values of individual segments from the URL's path. @@ -113,7 +113,7 @@ purl('http://allmarkedup.com/folder/dir/example/index.html').segment(); // plain ``` Fragment parameters and/or segments -------------------------------- +----------------------------------- Some sites and apps also use the hash fragment to store querystring-style key value pairs (eg. `http://test.com/#sky=blue&grass=green`), or slash-delimited paths (eg. `http://test.com/#/about/us/`). @@ -130,7 +130,7 @@ purl('http://test.com/#/about/us/').fsegment(1); // returns 'about' ``` Strict mode and relative URLs --------------------- +----------------------------- Internally this plugin uses Steven Levithan's excellent Regex URI parser, which has two modes - loose and strict. This plugin uses the loose mode by default (i.e. strict mode set to `false`), which deviates slightly from the specs but can produce more intuitive results in some situations. However, loose mode will not correctly parse relative URLs, so you can optionally enable strict mode when calling the plugin as follows: @@ -155,8 +155,51 @@ If there is a chance you may end up parsing a badly encoded URL you should proba Thanks to [steve78b](https://github.com/steve78b) for pointing this out. +Generate URL +------------ + +With *toString* method you can generate the url from url object. +You can use param method to modify parameters values. + +Example : + +``` javascript +/*---- jQuery version -----*/ +var url = $.url('http://www.example.com?item1=12'); +url.param('item2', 42); +url.toString(); // return "http://www.example.com?item1=12&item2=42" + + +url.param('item1', 20); +url.removeParam('item2'); +url.toString(); // return "http://www.example.com?item1=20" + +url.param({ + 'item5': 13, + 'item6': "foobar" +}); +url.toString(); // return "http://www.example.com?item5=13&item6=foobar" + +/*---- plain JS version -----*/ +var url = purl('http://www.example.com?item1=12'); +url.param('item2', 42); +url.toString(); // return "http://www.example.com?item1=12&item2=42" + + +url.param('item1', 20); +url.removeParam('item2'); +url.toString(); // return "http://www.example.com?item1=20" + +url.param({ + 'item5': 13, + 'item6': "foobar" +}); +url.toString(); // return "http://www.example.com?item5=13&item6=foobar" +``` + + Older versions and compatability ---------------------------------- +-------------------------------- Please note that v2.x is **not** backwards compatible with v1.x of this plugin. v1.1 is still [available for download](https://github.com/allmarkedup/jQuery-URL-Parser/zipball/v1.1) should you need it for some reason. @@ -179,7 +222,3 @@ $ buster static ``` Buster will then start up a server and give you a url (like `http://localhost:8956`) which you can navigate to with your browser of choice to see the test results. - - - - diff --git a/purl.js b/purl.js index 509ca08..b93a84c 100644 --- a/purl.js +++ b/purl.js @@ -5,267 +5,312 @@ * Licensed under an MIT-style license. See https://github.com/allmarkedup/jQuery-URL-Parser/blob/master/LICENSE for details. */ -;(function(factory) { - if (typeof define === 'function' && define.amd) { - // AMD available; use anonymous module - if ( typeof jQuery !== 'undefined' ) { - define(['jquery'], factory); - } else { - define([], factory); - } - } else { - // No AMD available; mutate global vars - if ( typeof jQuery !== 'undefined' ) { - factory(jQuery); - } else { - factory(); - } - } +(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD available; use anonymous module + if ( typeof jQuery !== 'undefined' ) { + define(['jquery'], factory); + } else { + define([], factory); + } + } else { + // No AMD available; mutate global vars + if ( typeof jQuery !== 'undefined' ) { + factory(jQuery); + } else { + factory(); + } + } })(function($, undefined) { - - var tag2attr = { - a : 'href', - img : 'src', - form : 'action', - base : 'href', - script : 'src', - iframe : 'src', - link : 'href' - }, - - key = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment'], // keys available to query - - aliases = { 'anchor' : 'fragment' }, // aliases for backwards compatability - - parser = { - strict : /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, //less intuitive, more accurate to the specs - loose : /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // more intuitive, fails on relative paths and deviates from specs - }, - - toString = Object.prototype.toString, - - isint = /^[0-9]+$/; - - function parseUri( url, strictMode ) { - var str = decodeURI( url ), - res = parser[ strictMode || false ? 'strict' : 'loose' ].exec( str ), - uri = { attr : {}, param : {}, seg : {} }, - i = 14; - - while ( i-- ) { - uri.attr[ key[i] ] = res[i] || ''; - } - - // build query and fragment parameters - uri.param['query'] = parseString(uri.attr['query']); - uri.param['fragment'] = parseString(uri.attr['fragment']); - - // split path and fragement into segments - uri.seg['path'] = uri.attr.path.replace(/^\/+|\/+$/g,'').split('/'); - uri.seg['fragment'] = uri.attr.fragment.replace(/^\/+|\/+$/g,'').split('/'); - - // compile a 'base' domain attribute - uri.attr['base'] = uri.attr.host ? (uri.attr.protocol ? uri.attr.protocol+'://'+uri.attr.host : uri.attr.host) + (uri.attr.port ? ':'+uri.attr.port : '') : ''; - - return uri; - }; - - function getAttrName( elm ) { - var tn = elm.tagName; - if ( typeof tn !== 'undefined' ) return tag2attr[tn.toLowerCase()]; - return tn; - } - - function promote(parent, key) { - if (parent[key].length == 0) return parent[key] = {}; - var t = {}; - for (var i in parent[key]) t[i] = parent[key][i]; - parent[key] = t; - return t; - } + + var tag2attr = { + a : 'href', + img : 'src', + form : 'action', + base : 'href', + script : 'src', + iframe : 'src', + link : 'href' + }, + + key = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment'], // keys available to query + + aliases = { 'anchor' : 'fragment' }, // aliases for backwards compatability + + parser = { + strict : /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, //less intuitive, more accurate to the specs + loose : /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // more intuitive, fails on relative paths and deviates from specs + }, + + toString = Object.prototype.toString, + + isint = /^[0-9]+$/; + + function parseUri( url, strictMode ) { + var str = decodeURI( url ), + res = parser[ strictMode || false ? 'strict' : 'loose' ].exec( str ), + uri = { attr : {}, param : {}, seg : {} }, + i = 14; + + while ( i-- ) { + uri.attr[ key[i] ] = res[i] || ''; + } + + // build query and fragment parameters + uri.param['query'] = parseString(uri.attr['query']); + uri.param['fragment'] = parseString(uri.attr['fragment']); + + // split path and fragement into segments + uri.seg['path'] = uri.attr.path.replace(/^\/+|\/+$/g,'').split('/'); + uri.seg['fragment'] = uri.attr.fragment.replace(/^\/+|\/+$/g,'').split('/'); + + // compile a 'base' domain attribute + uri.attr['base'] = uri.attr.host ? (uri.attr.protocol ? uri.attr.protocol+'://'+uri.attr.host : uri.attr.host) + (uri.attr.port ? ':'+uri.attr.port : '') : ''; + + return uri; + } + + function getAttrName( elm ) { + var tn = elm.tagName; + if ( typeof tn !== 'undefined' ) return tag2attr[tn.toLowerCase()]; + return tn; + } + + function promote(parent, key) { + if (parent[key].length === 0) return parent[key] = {}; + var t = {}; + for (var i in parent[key]) t[i] = parent[key][i]; + parent[key] = t; + return t; + } - function parse(parts, parent, key, val) { - var part = parts.shift(); - if (!part) { - if (isArray(parent[key])) { - parent[key].push(val); - } else if ('object' == typeof parent[key]) { - parent[key] = val; - } else if ('undefined' == typeof parent[key]) { - parent[key] = val; - } else { - parent[key] = [parent[key], val]; - } - } else { - var obj = parent[key] = parent[key] || []; - if (']' == part) { - if (isArray(obj)) { - if ('' != val) obj.push(val); - } else if ('object' == typeof obj) { - obj[keys(obj).length] = val; - } else { - obj = parent[key] = [parent[key], val]; - } - } else if (~part.indexOf(']')) { - part = part.substr(0, part.length - 1); - if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); - parse(parts, obj, part, val); - // key - } else { - if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); - parse(parts, obj, part, val); - } - } - } + function parse(parts, parent, key, val) { + var part = parts.shift(); + if (!part) { + if (isArray(parent[key])) { + parent[key].push(val); + } else if ('object' == typeof parent[key]) { + parent[key] = val; + } else if ('undefined' == typeof parent[key]) { + parent[key] = val; + } else { + parent[key] = [parent[key], val]; + } + } else { + var obj = parent[key] = parent[key] || []; + if (']' == part) { + if (isArray(obj)) { + if ('' != val) obj.push(val); + } else if ('object' == typeof obj) { + obj[keys(obj).length] = val; + } else { + obj = parent[key] = [parent[key], val]; + } + } else if (~part.indexOf(']')) { + part = part.substr(0, part.length - 1); + if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); + parse(parts, obj, part, val); + // key + } else { + if (!isint.test(part) && isArray(obj)) obj = promote(parent, key); + parse(parts, obj, part, val); + } + } + } - function merge(parent, key, val) { - if (~key.indexOf(']')) { - var parts = key.split('['), - len = parts.length, - last = len - 1; - parse(parts, parent, 'base', val); - } else { - if (!isint.test(key) && isArray(parent.base)) { - var t = {}; - for (var k in parent.base) t[k] = parent.base[k]; - parent.base = t; - } - set(parent.base, key, val); - } - return parent; - } + function merge(parent, key, val) { + if (~key.indexOf(']')) { + var parts = key.split('['), + len = parts.length, + last = len - 1; + parse(parts, parent, 'base', val); + } else { + if (!isint.test(key) && isArray(parent.base)) { + var t = {}; + for (var k in parent.base) t[k] = parent.base[k]; + parent.base = t; + } + set(parent.base, key, val); + } + return parent; + } - function parseString(str) { - return reduce(String(str).split(/&|;/), function(ret, pair) { - try { - pair = decodeURIComponent(pair.replace(/\+/g, ' ')); - } catch(e) { - // ignore - } - var eql = pair.indexOf('='), - brace = lastBraceInKey(pair), - key = pair.substr(0, brace || eql), - val = pair.substr(brace || eql, pair.length), - val = val.substr(val.indexOf('=') + 1, val.length); + function parseString(str) { + if (str == '') + return {}; - if ('' == key) key = pair, val = ''; + return reduce(String(str).split(/&|;/), function(ret, pair) { + try { + pair = decodeURIComponent(pair.replace(/\+/g, ' ')); + } catch(e) { + // ignore + } + var eql = pair.indexOf('='), + brace = lastBraceInKey(pair), + key = pair.substr(0, brace || eql), + val = pair.substr(brace || eql, pair.length), + val = val.substr(val.indexOf('=') + 1, val.length); - return merge(ret, key, val); - }, { base: {} }).base; - } - - function set(obj, key, val) { - var v = obj[key]; - if (undefined === v) { - obj[key] = val; - } else if (isArray(v)) { - v.push(val); - } else { - obj[key] = [v, val]; - } - } - - function lastBraceInKey(str) { - var len = str.length, - brace, c; - for (var i = 0; i < len; ++i) { - c = str[i]; - if (']' == c) brace = false; - if ('[' == c) brace = true; - if ('=' == c && !brace) return i; - } - } - - function reduce(obj, accumulator){ - var i = 0, - l = obj.length >> 0, - curr = arguments[2]; - while (i < l) { - if (i in obj) curr = accumulator.call(undefined, curr, obj[i], i, obj); - ++i; - } - return curr; - } - - function isArray(vArg) { - return Object.prototype.toString.call(vArg) === "[object Array]"; - } - - function keys(obj) { - var keys = []; - for ( prop in obj ) { - if ( obj.hasOwnProperty(prop) ) keys.push(prop); - } - return keys; - } - - function purl( url, strictMode ) { - if ( arguments.length === 1 && url === true ) { - strictMode = true; - url = undefined; - } - strictMode = strictMode || false; - url = url || window.location.toString(); - - return { - - data : parseUri(url, strictMode), - - // get various attributes from the URI - attr : function( attr ) { - attr = aliases[attr] || attr; - return typeof attr !== 'undefined' ? this.data.attr[attr] : this.data.attr; - }, - - // return query string parameters - param : function( param ) { - return typeof param !== 'undefined' ? this.data.param.query[param] : this.data.param.query; - }, - - // return fragment parameters - fparam : function( param ) { - return typeof param !== 'undefined' ? this.data.param.fragment[param] : this.data.param.fragment; - }, - - // return path segments - segment : function( seg ) { - if ( typeof seg === 'undefined' ) { - return this.data.seg.path; - } else { - seg = seg < 0 ? this.data.seg.path.length + seg : seg - 1; // negative segments count from the end - return this.data.seg.path[seg]; - } - }, - - // return fragment segments - fsegment : function( seg ) { - if ( typeof seg === 'undefined' ) { - return this.data.seg.fragment; - } else { - seg = seg < 0 ? this.data.seg.fragment.length + seg : seg - 1; // negative segments count from the end - return this.data.seg.fragment[seg]; - } - } - - }; - - }; - - if ( typeof $ !== 'undefined' ) { - - $.fn.url = function( strictMode ) { - var url = ''; - if ( this.length ) { - url = $(this).attr( getAttrName(this[0]) ) || ''; - } - return purl( url, strictMode ); - }; - - $.url = purl; - - } else { - window.purl = purl; - } + if ('' == key) key = pair, val = ''; -}); + return merge(ret, key, val); + }, { base: {} }).base; + } + + function set(obj, key, val) { + var v = obj[key]; + if (undefined === v) { + obj[key] = val; + } else if (isArray(v)) { + v.push(val); + } else { + obj[key] = [v, val]; + } + } + + function lastBraceInKey(str) { + var len = str.length, + brace, c; + for (var i = 0; i < len; ++i) { + c = str[i]; + if (']' == c) brace = false; + if ('[' == c) brace = true; + if ('=' == c && !brace) return i; + } + } + + function reduce(obj, accumulator){ + var i = 0, + l = obj.length >> 0, + curr = arguments[2]; + while (i < l) { + if (i in obj) curr = accumulator.call(undefined, curr, obj[i], i, obj); + ++i; + } + return curr; + } + + function isArray(vArg) { + return Object.prototype.toString.call(vArg) === "[object Array]"; + } + + function keys(obj) { + var keys = []; + for ( prop in obj ) { + if ( obj.hasOwnProperty(prop) ) keys.push(prop); + } + return keys; + } + + function isPlainObject(o) { + return Object(o) === o && Object.getPrototypeOf(o) === Object.prototype; + } + + function purl( url, strictMode ) { + if ( arguments.length === 1 && url === true ) { + strictMode = true; + url = undefined; + } + strictMode = strictMode || false; + url = url || window.location.toString(); + + return { + + data : parseUri(url, strictMode), + + // get various attributes from the URI + attr : function( attr ) { + attr = aliases[attr] || attr; + return typeof attr !== 'undefined' ? this.data.attr[attr] : this.data.attr; + }, + + // get or set query string parameters + param : function(param, value) { + if (isPlainObject(param)) { + this.data.param.query = param; + } else if (value == undefined) { + return typeof param !== 'undefined' ? this.data.param.query[param] : this.data.param.query; + } else { + this.data.param.query[param] = value; + } + }, + + removeParam: function(param) { + delete this.data.param.query[param]; + }, + + // return fragment parameters + fparam : function( param ) { + return typeof param !== 'undefined' ? this.data.param.fragment[param] : this.data.param.fragment; + }, + + // return path segments + segment : function( seg ) { + if ( typeof seg === 'undefined' ) { + return this.data.seg.path; + } else { + seg = seg < 0 ? this.data.seg.path.length + seg : seg - 1; // negative segments count from the end + return this.data.seg.path[seg]; + } + }, + + // return fragment segments + fsegment : function( seg ) { + if ( typeof seg === 'undefined' ) { + return this.data.seg.fragment; + } else { + seg = seg < 0 ? this.data.seg.fragment.length + seg : seg - 1; // negative segments count from the end + return this.data.seg.fragment[seg]; + } + }, + // Return generated url from object data + toString: function() { + var buffer = ""; + if (this.data.attr.host !== '') { + buffer += this.data.attr.protocol + '://' + this.data.attr.host; + } + buffer += this.data.attr.path; + if (Object.keys(this.data.param.query).length > 0) { + buffer += '?'; + var params_buffer = []; + for(var p in this.data.param.query) { + params_buffer.push(p + '=' + this.data.param.query[p]); + } + buffer += params_buffer.join('&'); + } + if (Object.keys(this.data.param.fragment).length > 0) { + buffer += '#'; + + var fragments_buffer = []; + for(var p in this.data.param.fragment) { + if (this.data.param.fragment[p] === '') { + fragments_buffer.push(p); + } else { + fragments_buffer.push(p + '=' + this.data.param.fragment[p]); + } + } + buffer += fragments_buffer.join('&'); + } + + return buffer; + } + + }; + } + + if ( typeof $ !== 'undefined' ) { + + $.fn.url = function( strictMode ) { + var url = ''; + if ( this.length ) { + url = $(this).attr( getAttrName(this[0]) ) || ''; + } + return purl( url, strictMode ); + }; + + $.url = purl; + } else { + window.purl = purl; + } +}); diff --git a/test/purl-tests.js b/test/purl-tests.js index d6e266d..7b71048 100644 --- a/test/purl-tests.js +++ b/test/purl-tests.js @@ -19,7 +19,7 @@ testSuite = function(url) { }); it('should have a relative path of /folder/dir/index.html?item=value#foo', function() { - expect(url.attr('relative')).toBe('/folder/dir/index.html?item=value#foo'); + expect(url.attr('relative')).toBe('/folder/dir/index.html?item=value&item2=foobar#foo'); }); it('should have a path of /folder/dir/index.html', function() { @@ -35,7 +35,7 @@ testSuite = function(url) { }); it('should have a querystring of item=value', function() { - expect(url.attr('query')).toBe('item=value'); + expect(url.attr('query')).toBe('item=value&item2=foobar'); }); it('should have an anchor of foo', function() { @@ -43,16 +43,18 @@ testSuite = function(url) { expect(url.attr('fragment')).toBe('foo'); }); - it('should have a param() of item: "value"', function() { - expect(url.param()).toBeObject({item: 'value'}) + it('should have a param() of item: "value", item2: "foobar"', function() { + var p = url.param(); + expect(p['item']).toBe('value'); + expect(p['item2']).toBe('foobar'); }); it('should have a param("item") of "value"', function() { - expect(url.param('item')).toBe('value') + expect(url.param('item')).toBe('value'); }); it('should have a segment() of ["folder","dir","index.html"]', function() { - expect(url.segment()).toEqual(["folder","dir","index.html"]) + expect(url.segment()).toEqual(["folder","dir","index.html"]); }); it('should have a segment(1) of "folder"', function() { @@ -62,18 +64,53 @@ testSuite = function(url) { it('should have a segment(-1) of "folder"', function() { expect(url.segment(-1)).toBe("index.html"); }); -} -describe("purl in non-strict mode", function () { + it('test toString', function() { + expect(url.toString()).toBe("http://allmarkedup.com/folder/dir/index.html?item=value&item2=foobar#foo"); + }); + + it('test toString after "item" param updated', function() { + url.param('item', 'value2'); + expect(url.toString()).toBe("http://allmarkedup.com/folder/dir/index.html?item=value2&item2=foobar#foo"); + }); + + it('test toString after "item2" param remove', function() { + url.removeParam('item2'); + expect(url.toString()).toBe("http://allmarkedup.com/folder/dir/index.html?item=value2#foo"); + }); + + it('test toString after append "item3" param', function() { + url.param('item3', 87); + expect(url.toString()).toBe("http://allmarkedup.com/folder/dir/index.html?item=value2&item3=87#foo"); + }); - testSuite(purl('http://allmarkedup.com/folder/dir/index.html?item=value#foo')); + it('test toString after replace all params', function() { + url.param({ + 'item4': 88, + 'item5': 42 + }); + expect(url.toString()).toBe("http://allmarkedup.com/folder/dir/index.html?item4=88&item5=42#foo"); + }); +}; +describe("purl in non-strict mode", function () { + testSuite(purl('http://allmarkedup.com/folder/dir/index.html?item=value&item2=foobar#foo')); }); describe("purl in strict mode", function () { - - testSuite(purl('http://allmarkedup.com/folder/dir/index.html?item=value#foo', + testSuite(purl('http://allmarkedup.com/folder/dir/index.html?item=value&item2=foobar#foo', true)); +}); +describe("url('/path/subpath')", function() { + it('param is {}', function() { + var url = purl('/path/subpath'); + assert.equals(url.param(), {}); + }); + it('test1', function() { + var url = purl('/path/subpath'); + console.log(url.toString()); + assert.equals(url.toString(), '/path/subpath'); + }); });