diff --git a/modules/angular2/src/services/url_resolver.dart b/modules/angular2/src/services/url_resolver.dart new file mode 100644 index 0000000000..6c8cd64cc6 --- /dev/null +++ b/modules/angular2/src/services/url_resolver.dart @@ -0,0 +1,26 @@ +library angular2.src.services.url_resolver; + +import 'package:angular2/di.dart' show Injectable; + +@Injectable() +class UrlResolver { + /** + * Resolves the `url` given the `baseUrl`: + * - when the `url` is null, the `baseUrl` is returned, + * - if `url` is relative ('path/to/here', './path/to/here'), the resolved url is a combination of + * `baseUrl` and `url`, + * - if `url` is absolute (it has a scheme: 'http://', 'https://' or start with '/'), the `url` is + * returned as is (ignoring the `baseUrl`) + * + * @param {string} baseUrl + * @param {string} url + * @returns {string} the resolved URL + */ + String resolve(String baseUrl, String url) { + Uri uri = Uri.parse(url); + if (uri.isAbsolute) return uri.toString(); + + Uri baseUri = Uri.parse(baseUrl); + return baseUri.resolveUri(uri).toString(); + } +} diff --git a/modules/angular2/src/services/url_resolver.ts b/modules/angular2/src/services/url_resolver.ts index 6457cb4ab7..d9636f5efd 100644 --- a/modules/angular2/src/services/url_resolver.ts +++ b/modules/angular2/src/services/url_resolver.ts @@ -1,54 +1,293 @@ import {Injectable} from 'angular2/di'; -import {isPresent, isBlank, RegExpWrapper, BaseException} from 'angular2/src/facade/lang'; -import {DOM} from 'angular2/src/dom/dom_adapter'; +import { + isPresent, + isBlank, + RegExpWrapper, + BaseException, + normalizeBlank +} from 'angular2/src/facade/lang'; +import {ListWrapper} from 'angular2/src/facade/collection'; @Injectable() export class UrlResolver { - static a; - - constructor() { - if (isBlank(UrlResolver.a)) { - UrlResolver.a = DOM.createElement('a'); - } - } - /** * Resolves the `url` given the `baseUrl`: * - when the `url` is null, the `baseUrl` is returned, - * - due to a limitation in the process used to resolve urls (a HTMLLinkElement), `url` must not - * start with a `/`, * - if `url` is relative ('path/to/here', './path/to/here'), the resolved url is a combination of * `baseUrl` and `url`, - * - if `url` is absolute (it has a scheme: 'http://', 'https://'), the `url` is returned - * (ignoring the `baseUrl`) + * - if `url` is absolute (it has a scheme: 'http://', 'https://' or start with '/'), the `url` is + * returned as is (ignoring the `baseUrl`) * * @param {string} baseUrl * @param {string} url * @returns {string} the resolved URL */ - resolve(baseUrl: string, url: string): string { - if (isBlank(url) || url == '') return baseUrl; - - if (url[0] == '/') { - // The `HTMLLinkElement` does not allow resolving this case (the `url` would be interpreted as - // relative): - // - `baseUrl` = 'http://www.foo.com/base' - // - `url` = '/absolute/path/to/here' - // - the result would be 'http://www.foo.com/base/absolute/path/to/here' while - // 'http://www.foo.com/absolute/path/to/here' - // is expected (without the 'base' segment). - throw new BaseException(`Could not resolve the url ${url} from ${baseUrl}`); - } - - var m = RegExpWrapper.firstMatch(_schemeRe, url); - - if (isPresent(m[1])) { - return url; - } - - DOM.resolveAndSetHref(UrlResolver.a, baseUrl, url); - return DOM.getHref(UrlResolver.a); - } + resolve(baseUrl: string, url: string): string { return _resolveUrl(baseUrl, url); } } -var _schemeRe = RegExpWrapper.create('^([^:/?#]+:)?'); +// The code below is adapted from Traceur: +// https://github.com/google/traceur-compiler/blob/9511c1dafa972bf0de1202a8a863bad02f0f95a8/src/runtime/url.js + +/** + * Builds a URI string from already-encoded parts. + * + * No encoding is performed. Any component may be omitted as either null or + * undefined. + * + * @param {?string=} opt_scheme The scheme such as 'http'. + * @param {?string=} opt_userInfo The user name before the '@'. + * @param {?string=} opt_domain The domain such as 'www.google.com', already + * URI-encoded. + * @param {(string|null)=} opt_port The port number. + * @param {?string=} opt_path The path, already URI-encoded. If it is not + * empty, it must begin with a slash. + * @param {?string=} opt_queryData The URI-encoded query data. + * @param {?string=} opt_fragment The URI-encoded fragment identifier. + * @return {string} The fully combined URI. + */ +function _buildFromEncodedParts(opt_scheme?: string, opt_userInfo?: string, opt_domain?: string, + opt_port?: string, opt_path?: string, opt_queryData?: string, + opt_fragment?: string): string { + var out = []; + + if (isPresent(opt_scheme)) { + out.push(opt_scheme + ':'); + } + + if (isPresent(opt_domain)) { + out.push('//'); + + if (isPresent(opt_userInfo)) { + out.push(opt_userInfo + '@'); + } + + out.push(opt_domain); + + if (isPresent(opt_port)) { + out.push(':' + opt_port); + } + } + + if (isPresent(opt_path)) { + out.push(opt_path); + } + + if (isPresent(opt_queryData)) { + out.push('?' + opt_queryData); + } + + if (isPresent(opt_fragment)) { + out.push('#' + opt_fragment); + } + + return out.join(''); +} + +/** + * A regular expression for breaking a URI into its component parts. + * + * {@link http://www.gbiv.com/protocols/uri/rfc/rfc3986.html#RFC2234} says + * As the "first-match-wins" algorithm is identical to the "greedy" + * disambiguation method used by POSIX regular expressions, it is natural and + * commonplace to use a regular expression for parsing the potential five + * components of a URI reference. + * + * The following line is the regular expression for breaking-down a + * well-formed URI reference into its components. + * + *
+ * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+ *  12            3  4          5       6  7        8 9
+ * 
+ * + * The numbers in the second line above are only to assist readability; they + * indicate the reference points for each subexpression (i.e., each paired + * parenthesis). We refer to the value matched for subexpression as $. + * For example, matching the above expression to + *
+ *     http://www.ics.uci.edu/pub/ietf/uri/#Related
+ * 
+ * results in the following subexpression matches: + *
+ *    $1 = http:
+ *    $2 = http
+ *    $3 = //www.ics.uci.edu
+ *    $4 = www.ics.uci.edu
+ *    $5 = /pub/ietf/uri/
+ *    $6 = 
+ *    $7 = 
+ *    $8 = #Related
+ *    $9 = Related
+ * 
+ * where indicates that the component is not present, as is the + * case for the query component in the above example. Therefore, we can + * determine the value of the five components as + *
+ *    scheme    = $2
+ *    authority = $4
+ *    path      = $5
+ *    query     = $7
+ *    fragment  = $9
+ * 
+ * + * The regular expression has been modified slightly to expose the + * userInfo, domain, and port separately from the authority. + * The modified version yields + *
+ *    $1 = http              scheme
+ *    $2 =        userInfo -\
+ *    $3 = www.ics.uci.edu   domain     | authority
+ *    $4 =        port     -/
+ *    $5 = /pub/ietf/uri/    path
+ *    $6 =        query without ?
+ *    $7 = Related           fragment without #
+ * 
+ * @type {!RegExp} + * @private + */ +var _splitRe = + RegExpWrapper.create('^' + + '(?:' + + '([^:/?#.]+)' + // scheme - ignore special characters + // used by other URL parts such as :, + // ?, /, #, and . + ':)?' + + '(?://' + + '(?:([^/?#]*)@)?' + // userInfo + '([\\w\\d\\-\\u0100-\\uffff.%]*)' + // domain - restrict to letters, + // digits, dashes, dots, percent + // escapes, and unicode characters. + '(?::([0-9]+))?' + // port + ')?' + + '([^?#]+)?' + // path + '(?:\\?([^#]*))?' + // query + '(?:#(.*))?' + // fragment + '$'); + +/** + * The index of each URI component in the return value of goog.uri.utils.split. + * @enum {number} + */ +enum _ComponentIndex { + SCHEME = 1, + USER_INFO, + DOMAIN, + PORT, + PATH, + QUERY_DATA, + FRAGMENT +} + +/** + * Splits a URI into its component parts. + * + * Each component can be accessed via the component indices; for example: + *
+ * goog.uri.utils.split(someStr)[goog.uri.utils.CompontentIndex.QUERY_DATA];
+ * 
+ * + * @param {string} uri The URI string to examine. + * @return {!Array.} Each component still URI-encoded. + * Each component that is present will contain the encoded value, whereas + * components that are not present will be undefined or empty, depending + * on the browser's regular expression implementation. Never null, since + * arbitrary strings may still look like path names. + */ +function _split(uri: string): List { + return RegExpWrapper.firstMatch(_splitRe, uri); +} + +/** + * Removes dot segments in given path component, as described in + * RFC 3986, section 5.2.4. + * + * @param {string} path A non-empty path component. + * @return {string} Path component with removed dot segments. + */ +function _removeDotSegments(path: string): string { + if (path == '/') return '/'; + + var leadingSlash = path[0] == '/' ? '/' : ''; + var trailingSlash = path[path.length - 1] === '/' ? '/' : ''; + var segments = path.split('/'); + + var out = []; + var up = 0; + for (var pos = 0; pos < segments.length; pos++) { + var segment = segments[pos]; + switch (segment) { + case '': + case '.': + break; + case '..': + if (out.length > 0) { + ListWrapper.removeAt(out, out.length - 1); + } else { + up++; + } + break; + default: + out.push(segment); + } + } + + if (leadingSlash == '') { + while (up-- > 0) { + ListWrapper.insert(out, 0, '..'); + } + + if (out.length === 0) out.push('.'); + } + + return leadingSlash + out.join('/') + trailingSlash; +} + +/** + * Takes an array of the parts from split and canonicalizes the path part + * and then joins all the parts. + * @param {Array.} parts + * @return {string} + */ +function _joinAndCanonicalizePath(parts: List): string { + var path = parts[_ComponentIndex.PATH]; + path = isBlank(path) ? '' : _removeDotSegments(path); + parts[_ComponentIndex.PATH] = path; + + return _buildFromEncodedParts(parts[_ComponentIndex.SCHEME], parts[_ComponentIndex.USER_INFO], + parts[_ComponentIndex.DOMAIN], parts[_ComponentIndex.PORT], path, + parts[_ComponentIndex.QUERY_DATA], parts[_ComponentIndex.FRAGMENT]); +} + +/** + * Resolves a URL. + * @param {string} base The URL acting as the base URL. + * @param {string} to The URL to resolve. + * @return {string} + */ +function _resolveUrl(base: string, url: string): string { + var parts = _split(url); + var baseParts = _split(base); + + if (isPresent(parts[_ComponentIndex.SCHEME])) { + return _joinAndCanonicalizePath(parts); + } else { + parts[_ComponentIndex.SCHEME] = baseParts[_ComponentIndex.SCHEME]; + } + + for (var i = _ComponentIndex.SCHEME; i <= _ComponentIndex.PORT; i++) { + if (isBlank(parts[i])) { + parts[i] = baseParts[i]; + } + } + + if (parts[_ComponentIndex.PATH][0] == '/') { + return _joinAndCanonicalizePath(parts); + } + + var path = baseParts[_ComponentIndex.PATH]; + if (isBlank(path)) path = '/'; + var index = path.lastIndexOf('/'); + path = path.substring(0, index + 1) + parts[_ComponentIndex.PATH]; + parts[_ComponentIndex.PATH] = path; + return _joinAndCanonicalizePath(parts); +} diff --git a/modules/angular2/test/services/url_resolver_spec.ts b/modules/angular2/test/services/url_resolver_spec.ts index 283c27dd72..b247214d66 100644 --- a/modules/angular2/test/services/url_resolver_spec.ts +++ b/modules/angular2/test/services/url_resolver_spec.ts @@ -5,44 +5,69 @@ export function main() { describe('UrlResolver', () => { var resolver = new UrlResolver(); - it('should add a relative path to the base url', () => { - expect(resolver.resolve('http://www.foo.com', 'bar')).toEqual('http://www.foo.com/bar'); - expect(resolver.resolve('http://www.foo.com/', 'bar')).toEqual('http://www.foo.com/bar'); - expect(resolver.resolve('http://www.foo.com', './bar')).toEqual('http://www.foo.com/bar'); - expect(resolver.resolve('http://www.foo.com/', './bar')).toEqual('http://www.foo.com/bar'); + describe('absolute base url', () => { + it('should add a relative path to the base url', () => { + expect(resolver.resolve('http://www.foo.com', 'bar')).toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com/', 'bar')).toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com', './bar')).toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com/', './bar')).toEqual('http://www.foo.com/bar'); + }); + + it('should replace the base path', () => { + expect(resolver.resolve('http://www.foo.com/baz', 'bar')).toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com/baz', './bar')) + .toEqual('http://www.foo.com/bar'); + }); + + it('should append to the base path', () => { + expect(resolver.resolve('http://www.foo.com/baz/', 'bar')) + .toEqual('http://www.foo.com/baz/bar'); + expect(resolver.resolve('http://www.foo.com/baz/', './bar')) + .toEqual('http://www.foo.com/baz/bar'); + }); + + it('should support ".." in the path', () => { + expect(resolver.resolve('http://www.foo.com/baz/', '../bar')) + .toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com/1/2/3/', '../../bar')) + .toEqual('http://www.foo.com/1/bar'); + expect(resolver.resolve('http://www.foo.com/1/2/3/', '../biz/bar')) + .toEqual('http://www.foo.com/1/2/biz/bar'); + expect(resolver.resolve('http://www.foo.com/1/2/baz', '../../bar')) + .toEqual('http://www.foo.com/bar'); + }); + + it('should ignore the base path when the url has a scheme', () => { + expect(resolver.resolve('http://www.foo.com', 'http://www.bar.com')) + .toEqual('http://www.bar.com'); + }); + + it('should support absolute urls', () => { + expect(resolver.resolve('http://www.foo.com', '/bar')).toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com/', '/bar')).toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com/baz', '/bar')) + .toEqual('http://www.foo.com/bar'); + expect(resolver.resolve('http://www.foo.com/baz/', '/bar')) + .toEqual('http://www.foo.com/bar'); + }); }); - it('should replace the base path', () => { - expect(resolver.resolve('http://www.foo.com/baz', 'bar')).toEqual('http://www.foo.com/bar'); - expect(resolver.resolve('http://www.foo.com/baz', './bar')).toEqual('http://www.foo.com/bar'); + describe('relative base url', () => { + it('should add a relative path to the base url', + () => { expect(resolver.resolve('foo/', './bar')).toEqual('foo/bar'); }); + + it('should replace the base path', + () => { expect(resolver.resolve('foo/baz', './bar')).toEqual('foo/bar'); }); + + it('should support ".." in the path', () => { + expect(resolver.resolve('foo/baz', '../bar')).toEqual('bar'); + expect(resolver.resolve('foo/baz', '../biz/bar')).toEqual('biz/bar'); + }); + + it('should support absolute urls', () => { + expect(resolver.resolve('foo/baz', '/bar')).toEqual('/bar'); + expect(resolver.resolve('foo/baz/', '/bar')).toEqual('/bar'); + }); }); - - it('should append to the base path', () => { - expect(resolver.resolve('http://www.foo.com/baz/', 'bar')) - .toEqual('http://www.foo.com/baz/bar'); - expect(resolver.resolve('http://www.foo.com/baz/', './bar')) - .toEqual('http://www.foo.com/baz/bar'); - }); - - it('should support ".." in the path', () => { - expect(resolver.resolve('http://www.foo.com/baz/', '../bar')) - .toEqual('http://www.foo.com/bar'); - expect(resolver.resolve('http://www.foo.com/1/2/3/', '../../bar')) - .toEqual('http://www.foo.com/1/bar'); - expect(resolver.resolve('http://www.foo.com/1/2/3/', '../biz/bar')) - .toEqual('http://www.foo.com/1/2/biz/bar'); - expect(resolver.resolve('http://www.foo.com/1/2/baz', '../../bar')) - .toEqual('http://www.foo.com/bar'); - }); - - it('should ignore the base path when the url has a scheme', - () => { - expect(resolver.resolve('http://www.foo.com', 'http://www.bar.com')) - .toEqual('http://www.bar.com'); - }) - - it('should throw when the url start with "/"', () => { - expect(() => { resolver.resolve('http://www.foo.com/1/2', '/test'); }).toThrowError(); - }); }); }