Skip to content

Commit 3875f02

Browse files
committed
refactor(UrlResolver): move away from the anchor link
fixes angular#2029 fixes angular#872
1 parent 06aaa0c commit 3875f02

File tree

3 files changed

+355
-65
lines changed

3 files changed

+355
-65
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
library angular2.src.services.url_resolver;
2+
3+
import 'package:angular2/di.dart' show Injectable;
4+
5+
@Injectable()
6+
class UrlResolver {
7+
/**
8+
* Resolves the `url` given the `baseUrl`:
9+
* - when the `url` is null, the `baseUrl` is returned,
10+
* - if `url` is relative ('path/to/here', './path/to/here'), the resolved url is a combination of
11+
* `baseUrl` and `url`,
12+
* - if `url` is absolute (it has a scheme: 'http://', 'https://' or start with '/'), the `url` is
13+
* returned as is (ignoring the `baseUrl`)
14+
*
15+
* @param {string} baseUrl
16+
* @param {string} url
17+
* @returns {string} the resolved URL
18+
*/
19+
String resolve(String baseUrl, String url) {
20+
Uri uri = Uri.parse(url);
21+
if (uri.isAbsolute) return uri.toString();
22+
23+
Uri baseUri = Uri.parse(baseUrl);
24+
return baseUri.resolveUri(uri).toString();
25+
}
26+
}
Lines changed: 271 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,293 @@
11
import {Injectable} from 'angular2/di';
2-
import {isPresent, isBlank, RegExpWrapper, BaseException} from 'angular2/src/facade/lang';
3-
import {DOM} from 'angular2/src/dom/dom_adapter';
2+
import {
3+
isPresent,
4+
isBlank,
5+
RegExpWrapper,
6+
BaseException,
7+
normalizeBlank
8+
} from 'angular2/src/facade/lang';
9+
import {ListWrapper} from 'angular2/src/facade/collection';
410

511
@Injectable()
612
export class UrlResolver {
7-
static a;
8-
9-
constructor() {
10-
if (isBlank(UrlResolver.a)) {
11-
UrlResolver.a = DOM.createElement('a');
12-
}
13-
}
14-
1513
/**
1614
* Resolves the `url` given the `baseUrl`:
1715
* - when the `url` is null, the `baseUrl` is returned,
18-
* - due to a limitation in the process used to resolve urls (a HTMLLinkElement), `url` must not
19-
* start with a `/`,
2016
* - if `url` is relative ('path/to/here', './path/to/here'), the resolved url is a combination of
2117
* `baseUrl` and `url`,
22-
* - if `url` is absolute (it has a scheme: 'http://', 'https://'), the `url` is returned
23-
* (ignoring the `baseUrl`)
18+
* - if `url` is absolute (it has a scheme: 'http://', 'https://' or start with '/'), the `url` is
19+
* returned as is (ignoring the `baseUrl`)
2420
*
2521
* @param {string} baseUrl
2622
* @param {string} url
2723
* @returns {string} the resolved URL
2824
*/
29-
resolve(baseUrl: string, url: string): string {
30-
if (isBlank(url) || url == '') return baseUrl;
31-
32-
if (url[0] == '/') {
33-
// The `HTMLLinkElement` does not allow resolving this case (the `url` would be interpreted as
34-
// relative):
35-
// - `baseUrl` = 'http://www.foo.com/base'
36-
// - `url` = '/absolute/path/to/here'
37-
// - the result would be 'http://www.foo.com/base/absolute/path/to/here' while
38-
// 'http://www.foo.com/absolute/path/to/here'
39-
// is expected (without the 'base' segment).
40-
throw new BaseException(`Could not resolve the url ${url} from ${baseUrl}`);
25+
resolve(baseUrl: string, url: string): string { return _resolveUrl(baseUrl, url); }
26+
}
27+
28+
// The code below is adapted from Traceur:
29+
// https://github.com/google/traceur-compiler/blob/9511c1dafa972bf0de1202a8a863bad02f0f95a8/src/runtime/url.js
30+
31+
/**
32+
* Builds a URI string from already-encoded parts.
33+
*
34+
* No encoding is performed. Any component may be omitted as either null or
35+
* undefined.
36+
*
37+
* @param {?string=} opt_scheme The scheme such as 'http'.
38+
* @param {?string=} opt_userInfo The user name before the '@'.
39+
* @param {?string=} opt_domain The domain such as 'www.google.com', already
40+
* URI-encoded.
41+
* @param {(string|null)=} opt_port The port number.
42+
* @param {?string=} opt_path The path, already URI-encoded. If it is not
43+
* empty, it must begin with a slash.
44+
* @param {?string=} opt_queryData The URI-encoded query data.
45+
* @param {?string=} opt_fragment The URI-encoded fragment identifier.
46+
* @return {string} The fully combined URI.
47+
*/
48+
function _buildFromEncodedParts(opt_scheme?: string, opt_userInfo?: string, opt_domain?: string,
49+
opt_port?: string, opt_path?: string, opt_queryData?: string,
50+
opt_fragment?: string): string {
51+
var out = [];
52+
53+
if (isPresent(opt_scheme)) {
54+
out.push(opt_scheme + ':');
55+
}
56+
57+
if (isPresent(opt_domain)) {
58+
out.push('//');
59+
60+
if (isPresent(opt_userInfo)) {
61+
out.push(opt_userInfo + '@');
4162
}
4263

43-
var m = RegExpWrapper.firstMatch(_schemeRe, url);
64+
out.push(opt_domain);
4465

45-
if (isPresent(m[1])) {
46-
return url;
66+
if (isPresent(opt_port)) {
67+
out.push(':' + opt_port);
4768
}
69+
}
4870

49-
DOM.resolveAndSetHref(UrlResolver.a, baseUrl, url);
50-
return DOM.getHref(UrlResolver.a);
71+
if (isPresent(opt_path)) {
72+
out.push(opt_path);
5173
}
74+
75+
if (isPresent(opt_queryData)) {
76+
out.push('?' + opt_queryData);
77+
}
78+
79+
if (isPresent(opt_fragment)) {
80+
out.push('#' + opt_fragment);
81+
}
82+
83+
return out.join('');
5284
}
5385

54-
var _schemeRe = RegExpWrapper.create('^([^:/?#]+:)?');
86+
/**
87+
* A regular expression for breaking a URI into its component parts.
88+
*
89+
* {@link http://www.gbiv.com/protocols/uri/rfc/rfc3986.html#RFC2234} says
90+
* As the "first-match-wins" algorithm is identical to the "greedy"
91+
* disambiguation method used by POSIX regular expressions, it is natural and
92+
* commonplace to use a regular expression for parsing the potential five
93+
* components of a URI reference.
94+
*
95+
* The following line is the regular expression for breaking-down a
96+
* well-formed URI reference into its components.
97+
*
98+
* <pre>
99+
* ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
100+
* 12 3 4 5 6 7 8 9
101+
* </pre>
102+
*
103+
* The numbers in the second line above are only to assist readability; they
104+
* indicate the reference points for each subexpression (i.e., each paired
105+
* parenthesis). We refer to the value matched for subexpression <n> as $<n>.
106+
* For example, matching the above expression to
107+
* <pre>
108+
* http://www.ics.uci.edu/pub/ietf/uri/#Related
109+
* </pre>
110+
* results in the following subexpression matches:
111+
* <pre>
112+
* $1 = http:
113+
* $2 = http
114+
* $3 = //www.ics.uci.edu
115+
* $4 = www.ics.uci.edu
116+
* $5 = /pub/ietf/uri/
117+
* $6 = <undefined>
118+
* $7 = <undefined>
119+
* $8 = #Related
120+
* $9 = Related
121+
* </pre>
122+
* where <undefined> indicates that the component is not present, as is the
123+
* case for the query component in the above example. Therefore, we can
124+
* determine the value of the five components as
125+
* <pre>
126+
* scheme = $2
127+
* authority = $4
128+
* path = $5
129+
* query = $7
130+
* fragment = $9
131+
* </pre>
132+
*
133+
* The regular expression has been modified slightly to expose the
134+
* userInfo, domain, and port separately from the authority.
135+
* The modified version yields
136+
* <pre>
137+
* $1 = http scheme
138+
* $2 = <undefined> userInfo -\
139+
* $3 = www.ics.uci.edu domain | authority
140+
* $4 = <undefined> port -/
141+
* $5 = /pub/ietf/uri/ path
142+
* $6 = <undefined> query without ?
143+
* $7 = Related fragment without #
144+
* </pre>
145+
* @type {!RegExp}
146+
* @private
147+
*/
148+
var _splitRe =
149+
RegExpWrapper.create('^' +
150+
'(?:' +
151+
'([^:/?#.]+)' + // scheme - ignore special characters
152+
// used by other URL parts such as :,
153+
// ?, /, #, and .
154+
':)?' +
155+
'(?://' +
156+
'(?:([^/?#]*)@)?' + // userInfo
157+
'([\\w\\d\\-\\u0100-\\uffff.%]*)' + // domain - restrict to letters,
158+
// digits, dashes, dots, percent
159+
// escapes, and unicode characters.
160+
'(?::([0-9]+))?' + // port
161+
')?' +
162+
'([^?#]+)?' + // path
163+
'(?:\\?([^#]*))?' + // query
164+
'(?:#(.*))?' + // fragment
165+
'$');
166+
167+
/**
168+
* The index of each URI component in the return value of goog.uri.utils.split.
169+
* @enum {number}
170+
*/
171+
enum _ComponentIndex {
172+
SCHEME = 1,
173+
USER_INFO,
174+
DOMAIN,
175+
PORT,
176+
PATH,
177+
QUERY_DATA,
178+
FRAGMENT
179+
}
180+
181+
/**
182+
* Splits a URI into its component parts.
183+
*
184+
* Each component can be accessed via the component indices; for example:
185+
* <pre>
186+
* goog.uri.utils.split(someStr)[goog.uri.utils.CompontentIndex.QUERY_DATA];
187+
* </pre>
188+
*
189+
* @param {string} uri The URI string to examine.
190+
* @return {!Array.<string|undefined>} Each component still URI-encoded.
191+
* Each component that is present will contain the encoded value, whereas
192+
* components that are not present will be undefined or empty, depending
193+
* on the browser's regular expression implementation. Never null, since
194+
* arbitrary strings may still look like path names.
195+
*/
196+
function _split(uri: string): List<string | any> {
197+
return RegExpWrapper.firstMatch(_splitRe, uri);
198+
}
199+
200+
/**
201+
* Removes dot segments in given path component, as described in
202+
* RFC 3986, section 5.2.4.
203+
*
204+
* @param {string} path A non-empty path component.
205+
* @return {string} Path component with removed dot segments.
206+
*/
207+
function _removeDotSegments(path: string): string {
208+
if (path == '/') return '/';
209+
210+
var leadingSlash = path[0] == '/' ? '/' : '';
211+
var trailingSlash = path[path.length - 1] === '/' ? '/' : '';
212+
var segments = path.split('/');
213+
214+
var out = [];
215+
var up = 0;
216+
for (var pos = 0; pos < segments.length; pos++) {
217+
var segment = segments[pos];
218+
switch (segment) {
219+
case '':
220+
case '.':
221+
break;
222+
case '..':
223+
if (out.length > 0) {
224+
ListWrapper.removeAt(out, out.length - 1);
225+
} else {
226+
up++;
227+
}
228+
break;
229+
default:
230+
out.push(segment);
231+
}
232+
}
233+
234+
if (leadingSlash == '') {
235+
while (up-- > 0) {
236+
ListWrapper.insert(out, 0, '..');
237+
}
238+
239+
if (out.length === 0) out.push('.');
240+
}
241+
242+
return leadingSlash + out.join('/') + trailingSlash;
243+
}
244+
245+
/**
246+
* Takes an array of the parts from split and canonicalizes the path part
247+
* and then joins all the parts.
248+
* @param {Array.<string?>} parts
249+
* @return {string}
250+
*/
251+
function _joinAndCanonicalizePath(parts: List<any>): string {
252+
var path = parts[_ComponentIndex.PATH];
253+
path = isBlank(path) ? '' : _removeDotSegments(path);
254+
parts[_ComponentIndex.PATH] = path;
255+
256+
return _buildFromEncodedParts(parts[_ComponentIndex.SCHEME], parts[_ComponentIndex.USER_INFO],
257+
parts[_ComponentIndex.DOMAIN], parts[_ComponentIndex.PORT], path,
258+
parts[_ComponentIndex.QUERY_DATA], parts[_ComponentIndex.FRAGMENT]);
259+
}
260+
261+
/**
262+
* Resolves a URL.
263+
* @param {string} base The URL acting as the base URL.
264+
* @param {string} to The URL to resolve.
265+
* @return {string}
266+
*/
267+
function _resolveUrl(base: string, url: string): string {
268+
var parts = _split(url);
269+
var baseParts = _split(base);
270+
271+
if (isPresent(parts[_ComponentIndex.SCHEME])) {
272+
return _joinAndCanonicalizePath(parts);
273+
} else {
274+
parts[_ComponentIndex.SCHEME] = baseParts[_ComponentIndex.SCHEME];
275+
}
276+
277+
for (var i = _ComponentIndex.SCHEME; i <= _ComponentIndex.PORT; i++) {
278+
if (isBlank(parts[i])) {
279+
parts[i] = baseParts[i];
280+
}
281+
}
282+
283+
if (parts[_ComponentIndex.PATH][0] == '/') {
284+
return _joinAndCanonicalizePath(parts);
285+
}
286+
287+
var path = baseParts[_ComponentIndex.PATH];
288+
if (isBlank(path)) path = '/';
289+
var index = path.lastIndexOf('/');
290+
path = path.substring(0, index + 1) + parts[_ComponentIndex.PATH];
291+
parts[_ComponentIndex.PATH] = path;
292+
return _joinAndCanonicalizePath(parts);
293+
}

0 commit comments

Comments
 (0)