/** * @ignore * Uri class for KISSY. * @author yiminghe@gmail.com */ (function (S, undefined) { var reDisallowedInSchemeOrUserInfo = /[#\/\?@]/g, reDisallowedInPathName = /[#\?]/g, // ?? combo of taobao reDisallowedInQuery = /[#@]/g, reDisallowedInFragment = /#/g, URI_SPLIT_REG = new RegExp( '^' + /* Scheme names consist of a sequence of characters beginning with a letter and followed by any combination of letters, digits, plus ('+'), period ('.'), or hyphen ('-'). */ '(?:([\\w\\d+.-]+):)?' + // scheme '(?://' + /* The authority component is preceded by a double slash ('//') and is terminated by the next slash ('/'), question mark ('?'), or number sign ('#') character, or by the end of the URI. */ '(?:([^/?#@]*)@)?' + // userInfo '(' + '[\\w\\d\\-\\u0100-\\uffff.+%]*' + '|' + // ipv6 '\\[[^\\]]+\\]' + ')' + // hostname - restrict to letters, // digits, dashes, dots, percent // escapes, and unicode characters. '(?::([0-9]+))?' + // port ')?' + /* The path is terminated by the first question mark ('?') or number sign ('#') character, or by the end of the URI. */ '([^?#]+)?' + // path. hierarchical part /* The query component is indicated by the first question mark ('?') character and terminated by a number sign ('#') character or by the end of the URI. */ '(?:\\?([^#]*))?' + // query. non-hierarchical data /* The fragment identifier component of a URI allows indirect identification of a secondary resource by reference to a primary resource and additional identifying information. A fragment identifier component is indicated by the presence of a number sign ('#') character and terminated by the end of the URI. */ '(?:#(.*))?' + // fragment '$'), Path = S.Path, REG_INFO = { scheme: 1, userInfo: 2, hostname: 3, port: 4, path: 5, query: 6, fragment: 7 }; function parseQuery(self) { if (!self._queryMap) { self._queryMap = S.unparam(self._query); } } /** * @class KISSY.Uri.Query * Query data structure. * @param {String} [query] encoded query string(without question mask). */ function Query(query) { this._query = query || ''; } Query.prototype = { constructor: Query, /** * Cloned new instance. * @return {KISSY.Uri.Query} */ clone: function () { return new Query(this.toString()); }, /** * reset to a new query string * @param {String} query * @chainable */ reset: function (query) { var self = this; self._query = query || ''; self._queryMap = null; return self; }, /** * Parameter count. * @return {Number} */ count: function () { var self = this, count = 0, _queryMap = self._queryMap, k; parseQuery(self); for (k in _queryMap) { if (S.isArray(_queryMap[k])) { count += _queryMap[k].length; } else { count++; } } return count; }, /** * judge whether has query parameter * @param {String} [key] */ has: function (key) { var self = this, _queryMap; parseQuery(self); _queryMap = self._queryMap; if (key) { return key in _queryMap; } else { return !S.isEmptyObject(_queryMap); } }, /** * Return parameter value corresponding to current key * @param {String} [key] */ get: function (key) { var self = this, _queryMap; parseQuery(self); _queryMap = self._queryMap; if (key) { return _queryMap[key]; } else { return _queryMap; } }, /** * Parameter names. * @return {String[]} */ keys: function () { var self = this; parseQuery(self); return S.keys(self._queryMap); }, /** * Set parameter value corresponding to current key * @param {String} key * @param value * @chainable */ set: function (key, value) { var self = this, _queryMap; parseQuery(self); _queryMap = self._queryMap; if (typeof key == 'string') { self._queryMap[key] = value; } else { if (key instanceof Query) { key = key.get(); } S.each(key, function (v, k) { _queryMap[k] = v; }); } return self; }, /** * Remove parameter with specified name. * @param {String} key * @chainable */ remove: function (key) { var self = this; parseQuery(self); if (key) { delete self._queryMap[key]; } else { self._queryMap = {}; } return self; }, /** * Add parameter value corresponding to current key * @param {String} key * @param value * @chainable */ add: function (key, value) { var self = this, _queryMap, currentValue; if (S.isObject(key)) { if (key instanceof Query) { key = key.get(); } S.each(key, function (v, k) { self.add(k, v); }); } else { parseQuery(self); _queryMap = self._queryMap; currentValue = _queryMap[key]; if (currentValue === undefined) { currentValue = value; } else { currentValue = [].concat(currentValue).concat(value); } _queryMap[key] = currentValue; } return self; }, /** * Serialize query to string. * @param {Boolean} [serializeArray=true] * whether append [] to key name when value 's type is array */ toString: function (serializeArray) { var self = this; parseQuery(self); return S.param(self._queryMap, undefined, undefined, serializeArray); } }; function padding2(str) { return str.length == 1 ? '0' + str : str; } function equalsIgnoreCase(str1, str2) { return str1.toLowerCase() == str2.toLowerCase(); } // www.ta#bao.com // => www.ta.com/#bao.com // www.ta%23bao.com // Percent-Encoding function encodeSpecialChars(str, specialCharsReg) { // encodeURI( ) is intended to encode complete URIs, // the following ASCII punctuation characters, // which have special meaning in URIs, are not escaped either: // ; / ? : @ & = + $ , # return encodeURI(str).replace(specialCharsReg, function (m) { return '%' + padding2(m.charCodeAt(0).toString(16)); }); } /** * @class KISSY.Uri * Uri class for KISSY. * Most of its interfaces are same with window.location. * @param {String|KISSY.Uri} [uriStr] Encoded uri string. */ function Uri(uriStr) { if (uriStr instanceof Uri) { return uriStr['clone'](); } var components, self = this; S.mix(self, { /** * scheme such as 'http:'. aka protocol without colon * @type {String} */ scheme: '', /** * User credentials such as 'yiminghe:gmail' * @type {String} */ userInfo: '', /** * hostname such as 'docs.kissyui.com'. aka domain * @type {String} */ hostname: '', /** * Port such as '8080' * @type {String} */ port: '', /** * path such as '/index.htm'. aka pathname * @type {String} */ path: '', /** * Query object for search string. aka search * @type {KISSY.Uri.Query} */ query: '', /** * fragment such as '#!/test/2'. aka hash */ fragment: '' }); components = Uri.getComponents(uriStr); S.each(components, function (v, key) { v = v || ''; if (key == 'query') { // need encoded content self.query = new Query(v); } else { // need to decode to get data structure in memory self[key] = S.urlDecode(v); } }); return self; } Uri.prototype = { constructor: Uri, /** * Return a cloned new instance. * @return {KISSY.Uri} */ clone: function () { var uri = new Uri(), self = this; S.each(REG_INFO, function (index, key) { uri[key] = self[key]; }); uri.query = uri.query.clone(); return uri; }, /** * The reference resolution algorithm.rfc 5.2 * return a resolved uri corresponding to current uri * @param {KISSY.Uri|String} relativeUri * * for example: * @example * this: 'http://y/yy/z.com?t=1#v=2' * 'https:/y/' => 'https:/y/' * '//foo' => 'http://foo' * 'foo' => 'http://y/yy/foo' * '/foo' => 'http://y/foo' * '?foo' => 'http://y/yy/z.com?foo' * '#foo' => http://y/yy/z.com?t=1#foo' * * @return {KISSY.Uri} */ resolve: function (relativeUri) { if (typeof relativeUri == 'string') { relativeUri = new Uri(relativeUri); } var self = this, override = 0, lastSlashIndex, order = ['scheme', 'userInfo', 'hostname', 'port', 'path', 'query', 'fragment'], target = self.clone(); S.each(order, function (o) { if (o == 'path') { // relativeUri does not set for scheme/userInfo/hostname/port if (override) { target[o] = relativeUri[o]; } else { var path = relativeUri['path']; if (path) { // force to override target 's query with relative override = 1; if (!S.startsWith(path, '/')) { if (target.hostname && !target.path) { // RFC 3986, section 5.2.3, case 1 path = '/' + path; } else if (target.path) { // RFC 3986, section 5.2.3, case 2 lastSlashIndex = target.path.lastIndexOf('/'); if (lastSlashIndex != -1) { path = target.path.slice(0, lastSlashIndex + 1) + path; } } } // remove .. / . as part of the resolution process target.path = Path.normalize(path); } } } else if (o == 'query') { if (override || relativeUri['query'].toString()) { target.query = relativeUri['query'].clone(); override = 1; } } else if (override || relativeUri[o]) { target[o] = relativeUri[o]; override = 1; } }); return target; }, /** * Get scheme part */ getScheme: function () { return this.scheme; }, /** * Set scheme part * @param {String} scheme * @chainable */ setScheme: function (scheme) { this.scheme = scheme; return this; }, /** * Return hostname * @return {String} */ getHostname: function () { return this.hostname; }, /** * Set hostname * @param {String} hostname * @chainable */ setHostname: function (hostname) { this.hostname = hostname; return this; }, /** * Set user info * @param {String} userInfo * @chainable */ 'setUserInfo': function (userInfo) { this.userInfo = userInfo; return this; }, /** * Get user info * @return {String} */ getUserInfo: function () { return this.userInfo; }, /** * Set port * @param {String} port * @chainable */ 'setPort': function (port) { this.port = port; return this; }, /** * Get port * @return {String} */ 'getPort': function () { return this.port; }, /** * Set path * @param {string} path * @chainable */ setPath: function (path) { this.path = path; return this; }, /** * Get path * @return {String} */ getPath: function () { return this.path; }, /** * Set query * @param {String|KISSY.Uri.Query} query * @chainable */ 'setQuery': function (query) { if (typeof query == 'string') { if (S.startsWith(query, '?')) { query = query.slice(1); } query = new Query(encodeSpecialChars(query, reDisallowedInQuery)); } this.query = query; return this; }, /** * Get query * @return {KISSY.Uri.Query} */ getQuery: function () { return this.query; }, /** * Get fragment * @return {String} */ getFragment: function () { return this.fragment; }, /** * Set fragment * @param {String} fragment * @chainable */ 'setFragment': function (fragment) { var self = this; if (S.startsWith(fragment, '#')) { fragment = fragment.slice(1); } self.fragment = fragment; return self; }, /** * Judge whether two uri has same domain. * @param {KISSY.Uri} other * @return {Boolean} */ isSameOriginAs: function (other) { var self = this; // port and hostname has to be same return equalsIgnoreCase(self.hostname, other['hostname']) && equalsIgnoreCase(self.scheme, other['scheme']) && equalsIgnoreCase(self.port, other['port']); }, /** * Serialize to string. * See rfc 5.3 Component Recomposition. * But kissy does not differentiate between undefined and empty. * @param {boolean} [serializeArray=true] * whether append [] to key name when value 's type is array * @return {String} */ toString: function (serializeArray) { var out = [], self = this, scheme, hostname, path, port, fragment, query, userInfo; if (scheme = self.scheme) { out.push(encodeSpecialChars(scheme, reDisallowedInSchemeOrUserInfo)); out.push(':'); } if (hostname = self.hostname) { out.push('//'); if (userInfo = self.userInfo) { out.push(encodeSpecialChars(userInfo, reDisallowedInSchemeOrUserInfo)); out.push('@'); } out.push(encodeURIComponent(hostname)); if (port = self.port) { out.push(':'); out.push(port); } } if (path = self.path) { if (hostname && !S.startsWith(path, '/')) { path = '/' + path; } path = Path.normalize(path); out.push(encodeSpecialChars(path, reDisallowedInPathName)); } if (query = ( self.query.toString.call(self.query, serializeArray))) { out.push('?'); out.push(query); } if (fragment = self.fragment) { out.push('#'); out.push(encodeSpecialChars(fragment, reDisallowedInFragment)) } return out.join(''); } }; Uri.Query = Query; Uri.getComponents = function (url) { url = url || ""; var m = url.match(URI_SPLIT_REG) || [], ret = {}; S.each(REG_INFO, function (index, key) { ret[key] = m[index]; }); return ret; }; S.Uri = Uri; })(KISSY); /* Refer - application/x-www-form-urlencoded - http://www.ietf.org/rfc/rfc3986.txt - http://en.wikipedia.org/wiki/URI_scheme - http://unixpapa.com/js/querystring.html - http://code.stephenmorley.org/javascript/parsing-query-strings-for-get-data/ - same origin: http://tools.ietf.org/html/rfc6454 */