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();
- });
});
}