src/uri.js
author Laurens Holst <laurens.hg@grauw.nl>
Thu Mar 03 23:03:08 2011 +0100 (11 months ago)
changeset 321 455c0717bf5c
parent 300 56522f1c4a18
permissions -rw-r--r--
Replace Ant build with primitive Maven build.
     1 /**
     2  * @fileOverview
     3  * 
     4  * Grauw URI utilities
     5  * 
     6  * See: http://hg.grauw.nl/grauw-lib/file/tip/src/uri.js
     7  * 
     8  * @author Laurens Holst (http://www.grauw.nl/)
     9  * 
    10  *   Copyright 2010 Laurens Holst
    11  * 
    12  *   Licensed under the Apache License, Version 2.0 (the "License");
    13  *   you may not use this file except in compliance with the License.
    14  *   You may obtain a copy of the License at
    15  * 
    16  *       http://www.apache.org/licenses/LICENSE-2.0
    17  * 
    18  *   Unless required by applicable law or agreed to in writing, software
    19  *   distributed under the License is distributed on an "AS IS" BASIS,
    20  *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    21  *   See the License for the specific language governing permissions and
    22  *   limitations under the License.
    23  * 
    24  */
    25 gl.module('gl.uri', [], function() {
    26 	
    27 	/**
    28 	 * Constructs a URI object.
    29 	 * @constructor
    30 	 * @class Implementation of URI parsing and base URI resolving algorithm in RFC 3986.
    31 	 * @param {string|URI} uri A string or URI object to create the object from.
    32 	 */
    33 	var URI = function(vURI) {
    34 		if (vURI instanceof URI) {	// copy constructor
    35 			this.scheme = vURI.scheme;
    36 			this.authority = vURI.authority;
    37 			this.path = vURI.path;
    38 			this.query = vURI.query;
    39 			this.fragment = vURI.fragment;
    40 		} else if (vURI) { // vURI is URI string or cast to string
    41 			var c = oParseRegex.exec(vURI);
    42 			this.scheme = c[1];
    43 			this.authority = c[2];
    44 			this.path = c[3];
    45 			this.query = c[4];
    46 			this.fragment = c[5];
    47 		}
    48 	};
    49 	
    50 	// Initial values on the prototype
    51 	URI.prototype.scheme    = null;
    52 	URI.prototype.authority = null;
    53 	URI.prototype.path      = '';
    54 	URI.prototype.query     = null;
    55 	URI.prototype.fragment  = null;
    56 	
    57 	// Regular expression from RFC 3986 appendix B
    58 	var oParseRegex = new RegExp('^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\\?([^#]*))?(?:#(.*))?$');
    59 
    60 	/**
    61 	 * Returns the scheme part of the URI.
    62 	 * In "http://example.com:80/a/b?x#y" this is "http".
    63 	 * @return
    64 	 */
    65 	URI.prototype.getScheme = function() {
    66 		return this.scheme;
    67 	};
    68 	
    69 	/**
    70 	 * Returns the authority part of the URI.
    71 	 * In "http://example.com:80/a/b?x#y" this is "example.com:80".
    72 	 * @return
    73 	 */
    74 	URI.prototype.getAuthority = function() {
    75 		return this.authority;
    76 	};
    77 	
    78 	/**
    79 	 * Returns the path part of the URI.
    80 	 * In "http://example.com:80/a/b?x#y" this is "/a/b".
    81 	 * In "mailto:mike@example.com" this is "mike@example.com".
    82 	 * @return
    83 	 */
    84 	URI.prototype.getPath = function() {
    85 		return this.path;
    86 	};
    87 	
    88 	/**
    89 	 * Returns the query part of the URI.
    90 	 * In "http://example.com:80/a/b?x#y" this is "x".
    91 	 * @return
    92 	 */
    93 	URI.prototype.getQuery = function() {
    94 		return this.query;
    95 	};
    96 	
    97 	/**
    98 	 * Returns the fragment part of the URI.
    99 	 * In "http://example.com:80/a/b?x#y" this is "y".
   100 	 * @return
   101 	 */
   102 	URI.prototype.getFragment = function() {
   103 		return this.fragment;
   104 	};
   105 	
   106 	/**
   107 	 * Tests whether the URI is an absolute URI.
   108 	 * See RFC 3986 section 4.3.
   109 	 */
   110 	URI.prototype.isAbsolute = function() {
   111 		return this.scheme && !this.fragment;
   112 	};
   113 	
   114 	///**
   115 	//* Extensive validation of the URI against the ABNF in RFC 3986
   116 	//*/
   117 	//URI.prototype.validate
   118 	
   119 	/**
   120 	 * Tests whether the URI is a same-document reference.
   121 	 * See RFC 3986 section 4.4.
   122 	 * 
   123 	 * To perform more thorough comparison, you can normalise the URI objects.
   124 	 */
   125 	URI.prototype.isSameDocumentAs = function(oURI) {
   126 		return oURI.scheme == this.scheme &&
   127 		    oURI.authority == this.authority &&
   128 		         oURI.path == this.path &&
   129 		        oURI.query == this.query;
   130 	};
   131 	
   132 	/**
   133 	 * Simple String Comparison of two URIs.
   134 	 * See RFC 3986 section 6.2.1.
   135 	 * 
   136 	 * To perform more thorough comparison, you can normalise the URI objects.
   137 	 */
   138 	URI.prototype.equals = function(oURI) {
   139 		return this.isSameDocumentAs(oURI) && oURI.fragment == this.fragment;
   140 	};
   141 	
   142 	/**
   143 	 * Normalizes the URI using syntax-based normalization.
   144 	 * This includes case normalization, percent-encoding normalization and path segment normalization.
   145 	 * XXX: Percent-encoding normalization does not escape characters that need to be escaped.
   146 	 *      (Although that would not be a valid URI in the first place. See validate().)
   147 	 * See RFC 3986 section 6.2.2.
   148 	 */
   149 	URI.prototype.normalize = function() {
   150 		this.removeDotSegments();
   151 		if (this.scheme)
   152 			this.scheme = this.scheme.toLowerCase();
   153 		if (this.authority)
   154 			this.authority = this.authority.replace(oAuthorityRegex, fAuthority).
   155 									replace(oCaseRegex, fCase);
   156 		if (this.path)
   157 			this.path = this.path.replace(oCaseRegex, fCase);
   158 		if (this.query)
   159 			this.query = this.query.replace(oCaseRegex, fCase);
   160 		if (this.fragment)
   161 			this.fragment = this.fragment.replace(oCaseRegex, fCase);
   162 	};
   163 	
   164 	var oCaseRegex = /%[0-9a-z]{2}/gi;
   165 	var oPercentRegex = /[a-zA-Z0-9\-\._~]/;
   166 	var oAuthorityRegex = /(.*@)?([^@:]*)(:.*)?/;
   167 	
   168 	function fCase(sStr) {
   169 		var sDec = unescape(sStr);
   170 		return oPercentRegex.test(sDec) ? sDec : sStr.toUpperCase();
   171 	}
   172 	
   173 	function fAuthority(sStr, p1, p2, p3) {
   174 		return (p1 || '') + p2.toLowerCase() + (p3 || '');
   175 	}
   176 	
   177 	/**
   178 	 * Resolve a relative URI (this) against a base URI.
   179 	 * The base URI must be an absolute URI.
   180 	 * See RFC 3986 section 5.2
   181 	 */
   182 	URI.prototype.resolve = function(oBaseURI) {
   183 		var oURI = new URI();
   184 		if (this.scheme) {
   185 			oURI.scheme = this.scheme;
   186 			oURI.authority = this.authority;
   187 			oURI.path = this.path;
   188 			oURI.query = this.query;
   189 		} else {
   190 			oURI.scheme = oBaseURI.scheme;
   191 			if (this.authority) {
   192 				oURI.authority = this.authority;
   193 				oURI.path = this.path;
   194 				oURI.query = this.query;
   195 			} else {
   196 				oURI.authority = oBaseURI.authority;
   197 				if (this.path == '') {
   198 					oURI.path = oBaseURI.path;
   199 					oURI.query = this.query || oBaseURI.query;
   200 				} else {
   201 					if (this.path.charAt(0) == '/') {
   202 						oURI.path = this.path;
   203 						oURI.removeDotSegments();
   204 					} else {
   205 						if (oBaseURI.authority && oBaseURI.path == '') {
   206 							oURI.path = '/' + this.path;
   207 						} else {
   208 							oURI.path = oBaseURI.path.substring(0, oBaseURI.path.lastIndexOf('/') + 1) + this.path;
   209 						}
   210 						oURI.removeDotSegments();
   211 					}
   212 					oURI.query = this.query;
   213 				}
   214 			}
   215 		}
   216 		oURI.fragment = this.fragment;
   217 		return oURI;
   218 	};
   219 	
   220 	/**
   221 	 * Remove dot segments from path.
   222 	 * See RFC 3986 section 5.2.4
   223 	 * @private
   224 	 */
   225 	URI.prototype.removeDotSegments = function() {
   226 		var aInput = this.path.split('/'),
   227 			aOutput = [],
   228 			sSegment,
   229 			bAbsPath = aInput[0] == '';
   230 		if (bAbsPath)
   231 			aInput.shift();
   232 		var sFirst = aInput[0] == '' ? aInput.shift() : null;
   233 		while (aInput.length) {
   234 			sSegment = aInput.shift();
   235 			if (sSegment == '..') {
   236 				aOutput.pop();
   237 			} else if (sSegment != '.') {
   238 				aOutput.push(sSegment);
   239 			}
   240 		}
   241 		if (sSegment == '.' || sSegment == '..')
   242 			aOutput.push('');
   243 		if (bAbsPath)
   244 			aOutput.unshift('');
   245 		this.path = aOutput.join('/');
   246 	};
   247 	
   248 	/**
   249 	 * Resolves a relative URI against an absolute base URI.
   250 	 * Convenience method.
   251 	 * @param {String} uri the relative URI to resolve
   252 	 * @param {String} baseURI the base URI (must be absolute) to resolve against
   253 	 */
   254 	URI.resolve = function(sURI, sBaseURI) {
   255 		var oURI = hCache[sURI] || (hCache[sURI] = new URI(sURI));
   256 		var oBaseURI = hCache[sBaseURI] || (hCache[sBaseURI] = new URI(sBaseURI));
   257 		return oURI.resolve(oBaseURI).toString();
   258 	};
   259 	
   260 	var hCache = {};
   261 	
   262 	/**
   263 	 * Serialises the URI to a string.
   264 	 */
   265 	URI.prototype.toString = function() {
   266 		var sResult = '';
   267 		if (this.scheme)
   268 			sResult += this.scheme + ':';
   269 		if (this.authority)
   270 			sResult += '//' + this.authority;
   271 		sResult += this.path;
   272 		if (this.query)
   273 			sResult += '?' + this.query;
   274 		if (this.fragment)
   275 			sResult += '#' + this.fragment;
   276 		return sResult;
   277 	};
   278 	
   279 	
   280 	
   281 	
   282 	// exports
   283 	this.URI = URI;
   284 
   285 });