refactor(UrlResolver): move away from the anchor link

fixes #2029
fixes #872
This commit is contained in:
Victor Berchet 2015-06-23 12:05:09 +02:00
parent 06aaa0c50e
commit 3875f02a52
3 changed files with 364 additions and 74 deletions

View File

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

View File

@ -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.
*
* <pre>
* ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
* 12 3 4 5 6 7 8 9
* </pre>
*
* 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 <n> as $<n>.
* For example, matching the above expression to
* <pre>
* http://www.ics.uci.edu/pub/ietf/uri/#Related
* </pre>
* results in the following subexpression matches:
* <pre>
* $1 = http:
* $2 = http
* $3 = //www.ics.uci.edu
* $4 = www.ics.uci.edu
* $5 = /pub/ietf/uri/
* $6 = <undefined>
* $7 = <undefined>
* $8 = #Related
* $9 = Related
* </pre>
* where <undefined> 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
* <pre>
* scheme = $2
* authority = $4
* path = $5
* query = $7
* fragment = $9
* </pre>
*
* The regular expression has been modified slightly to expose the
* userInfo, domain, and port separately from the authority.
* The modified version yields
* <pre>
* $1 = http scheme
* $2 = <undefined> userInfo -\
* $3 = www.ics.uci.edu domain | authority
* $4 = <undefined> port -/
* $5 = /pub/ietf/uri/ path
* $6 = <undefined> query without ?
* $7 = Related fragment without #
* </pre>
* @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:
* <pre>
* goog.uri.utils.split(someStr)[goog.uri.utils.CompontentIndex.QUERY_DATA];
* </pre>
*
* @param {string} uri The URI string to examine.
* @return {!Array.<string|undefined>} 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<string | any> {
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.<string?>} parts
* @return {string}
*/
function _joinAndCanonicalizePath(parts: List<any>): 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);
}

View File

@ -5,6 +5,7 @@ export function main() {
describe('UrlResolver', () => {
var resolver = new UrlResolver();
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');
@ -14,7 +15,8 @@ export function main() {
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');
expect(resolver.resolve('http://www.foo.com/baz', './bar'))
.toEqual('http://www.foo.com/bar');
});
it('should append to the base path', () => {
@ -35,14 +37,37 @@ export function main() {
.toEqual('http://www.foo.com/bar');
});
it('should ignore the base path when the url has a scheme',
() => {
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();
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');
});
});
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');
});
});
});
}