Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit 50e2645

Browse files
Diana Salsburychirayuk
Diana Salsbury
authored andcommitted
feat(urls): support relative CSS / template URLs in components
Example: When defining a component called 'foo' in file '/myproject/has/many/folders/foo.dart', prior to the change you would do so thusly: @component( selector: 'foo', templateUrl: '/myproject/has/many/folders/foo.html', cssUrl: '/myproject/has/many/folders/foo.css' ) The above is still valid, but now can also be defined rather more succinctly as: @component( selector: 'foo', templateUrl: 'foo.html', cssUrl: 'foo.css' ) A full table of what URLs will be changed/unchanged is below: You Say: Transformer replies: 'packages/foo/bar.html' 'packages/foo/bar.html' 'foo.html' 'packages/foo/bar.html' './foo.html' 'packages/foo/bar.html' '/foo.html' '/foo.html' 'http://google.com/foo.html' 'http://google.com/foo.html' *Note that as shown any absolute paths you define will not be bothered by the transformer, so if you have a templateUrl living at <root>/foo.html, you can still reach it with '/foo.html'! BREAKING CHANGE: You must update relative paths to your templates and in ng-include's to be relative to the component's library / ng-include'd file. NOTE: This feature is defaulted to an "on" state. To get back the old behavior, you may disable this feature for your application. This is only to help you adjust to this change and will go away in a later version. Here's how you can get the old behavior: ```dart module.bind(ResourceResolverConfig, toValue: new ResourceResolverConfig.useRelativeUrls(false)); ``` *Testing*: - e2e transformer tests can now be done on the sample application found in the test_transformers folder
1 parent 4e23817 commit 50e2645

38 files changed

+1356
-183
lines changed

lib/application_factory.dart

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'package:angular/change_detection/change_detection.dart';
1616
import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart';
1717
import 'package:angular/core/registry_dynamic.dart';
1818
import 'package:angular/core/parser/dynamic_closure_map.dart';
19+
import 'package:angular/core_dom/type_to_uri_mapper.dart';
20+
import 'package:angular/core_dom/type_to_uri_mapper_dynamic.dart';
1921
import 'dart:html';
2022

2123
/**
@@ -42,6 +44,7 @@ import 'dart:html';
4244
'angular.core.parser.Parser',
4345
'angular.core.parser',
4446
'angular.core.parser.lexer',
47+
'angular.core_dom.type_to_uri_mapper_dynamic',
4548
'angular.core_dynamic.DynamicMetadataExtractor',
4649
'perf_api',
4750
List,
@@ -58,6 +61,7 @@ import 'dart:mirrors' show MirrorsUsed;
5861
class _DynamicApplication extends Application {
5962
_DynamicApplication() {
6063
ngModule
64+
..bind(TypeToUriMapper, toImplementation: DynamicTypeToUriMapper)
6165
..bind(MetadataExtractor, toImplementation: DynamicMetadataExtractor)
6266
..bind(FieldGetterFactory, toImplementation: DynamicFieldGetterFactory)
6367
..bind(ClosureMap, toImplementation: DynamicClosureMap);

lib/application_factory_static.dart

+21-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import 'package:angular/application.dart';
2828
import 'package:angular/core/registry.dart';
2929
import 'package:angular/core/parser/parser.dart';
3030
import 'package:angular/core/parser/static_closure_map.dart';
31+
import 'package:angular/core_dom/type_to_uri_mapper.dart';
3132
import 'package:angular/core/registry_static.dart';
3233
import 'package:angular/change_detection/change_detection.dart';
3334
import 'package:angular/change_detection/dirty_checking_change_detector_static.dart';
@@ -41,8 +42,10 @@ class _StaticApplication extends Application {
4142
Map<Type, Object> metadata,
4243
Map<String, FieldGetter> fieldGetters,
4344
Map<String, FieldSetter> fieldSetters,
44-
Map<String, Symbol> symbols) {
45+
Map<String, Symbol> symbols,
46+
TypeToUriMapper uriMapper) {
4547
ngModule
48+
..bind(TypeToUriMapper, toValue: uriMapper)
4649
..bind(MetadataExtractor, toValue: new StaticMetadataExtractor(metadata))
4750
..bind(FieldGetterFactory, toValue: new StaticFieldGetterFactory(fieldGetters))
4851
..bind(ClosureMap, toValue: new StaticClosureMap(fieldGetters, fieldSetters, symbols));
@@ -76,10 +79,25 @@ class _StaticApplication extends Application {
7679
* .run();
7780
*
7881
*/
82+
83+
class _NullUriMapper extends TypeToUriMapper {
84+
Uri uriForType(Type type) {
85+
throw "You did not pass in a TypeToUriMapper to your StaticApplicationFactory."
86+
"(This would have been automatic if you used Dart transformers.) You must pass "
87+
"in a valid TypeTpUriMapper when constructing your Static Application";
88+
}
89+
}
7990
Application staticApplicationFactory(
8091
Map<Type, Object> metadata,
8192
Map<String, FieldGetter> fieldGetters,
8293
Map<String, FieldSetter> fieldSetters,
83-
Map<String, Symbol> symbols) {
84-
return new _StaticApplication(metadata, fieldGetters, fieldSetters, symbols);
94+
Map<String, Symbol> symbols,
95+
[TypeToUriMapper uriMapper]) {
96+
// TODO(chirayu): uriMapper is optional for backwards compatibility. Make it
97+
// a required parameter by the next release.
98+
if (uriMapper == null) {
99+
uriMapper = new _NullUriMapper();
100+
}
101+
return new _StaticApplication(metadata, fieldGetters, fieldSetters,
102+
symbols, uriMapper);
85103
}

lib/core/module.dart

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export "package:angular/core_dom/module_internal.dart" show
5656
RequestInterceptor,
5757
Response,
5858
ResponseError,
59+
ResourceResolverConfig,
60+
ResourceUrlResolver,
5961
UrlRewriter,
6062
TemplateCache,
6163
View,

lib/core_dom/module_internal.dart

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import 'package:angular/core_dom/static_keys.dart';
1919
import 'package:angular/core_dom/directive_injector.dart';
2020
export 'package:angular/core_dom/directive_injector.dart' show DirectiveInjector;
2121

22+
import 'package:angular/core_dom/type_to_uri_mapper.dart';
23+
import 'package:angular/core_dom/resource_url_resolver.dart';
24+
export 'package:angular/core_dom/resource_url_resolver.dart'
25+
show ResourceUrlResolver, ResourceResolverConfig;
26+
2227
import 'package:angular/change_detection/watch_group.dart' show Watch, PrototypeMap;
2328
import 'package:angular/change_detection/ast_parser.dart';
2429
import 'package:angular/core/registry.dart';
@@ -80,13 +85,15 @@ class CoreDomModule extends Module {
8085
bind(ComponentCssRewriter);
8186
bind(WebPlatform);
8287

88+
bind(ResourceUrlResolver);
8389
bind(Http);
8490
bind(UrlRewriter);
8591
bind(HttpBackend);
8692
bind(HttpDefaultHeaders);
8793
bind(HttpDefaults);
8894
bind(HttpInterceptors);
8995
bind(HttpConfig);
96+
bind(ResourceResolverConfig);
9097
bind(Animate);
9198
bind(ViewCache);
9299
bind(BrowserCookies);
+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* Dart port of
3+
* https://github.com/Polymer/platform-dev/blob/896e245a0046a397bfc0190d958d2bd162e8f53c/src/url.js
4+
*
5+
* This converts URIs within a document from relative URIs to being absolute URIs.
6+
*/
7+
8+
library angular.core_dom.resource_url_resolver;
9+
10+
import 'dart:html';
11+
import 'dart:js' as js;
12+
13+
import 'package:di/di.dart';
14+
import 'package:di/annotations.dart';
15+
16+
import 'package:angular/core_dom/type_to_uri_mapper.dart';
17+
18+
@Injectable()
19+
class ResourceUrlResolver {
20+
static final RegExp cssUrlRegexp = new RegExp(r'''(\burl\((?:[\s]+)?)(['"]?)([^]*)(\2(?:[\s]+)?\))''');
21+
static final RegExp cssImportRegexp = new RegExp(r'(@import[\s]+(?!url\())([^;]*)(;)');
22+
static const List<String> urlAttrs = const ['href', 'src', 'action'];
23+
static final String urlAttrsSelector = '[${urlAttrs.join('],[')}]';
24+
static final RegExp urlTemplateSearch = new RegExp('{{.*}}');
25+
static final RegExp quotes = new RegExp("[\"\']");
26+
27+
// Ensures that Uri.base is http/https.
28+
final _baseUri = Uri.base.origin + ("/");
29+
30+
final TypeToUriMapper _uriMapper;
31+
final ResourceResolverConfig _config;
32+
33+
ResourceUrlResolver(this._uriMapper, this._config);
34+
35+
String resolveHtml(String html, [Uri baseUri]) {
36+
if (baseUri == null) {
37+
return html;
38+
}
39+
HtmlDocument document = new DomParser().parseFromString(
40+
"<!doctype html><html><body>$html</body></html>", "text/html");
41+
_resolveDom(document.body, baseUri);
42+
return document.body.innerHtml;
43+
}
44+
45+
/**
46+
* Resolves all relative URIs within the DOM from being relative to
47+
* [originalBase] to being absolute.
48+
*/
49+
void _resolveDom(Node root, Uri baseUri) {
50+
_resolveAttributes(root, baseUri);
51+
_resolveStyles(root, baseUri);
52+
53+
// handle template.content
54+
for (var template in _querySelectorAll(root, 'template')) {
55+
if (template.content != null) {
56+
_resolveDom(template.content, baseUri);
57+
}
58+
}
59+
}
60+
61+
Iterable<Element> _querySelectorAll(Node node, String selectors) {
62+
if (node is DocumentFragment) {
63+
return node.querySelectorAll(selectors);
64+
}
65+
if (node is Element) {
66+
return node.querySelectorAll(selectors);
67+
}
68+
return const [];
69+
}
70+
71+
void _resolveStyles(Node node, Uri baseUri) {
72+
var styles = _querySelectorAll(node, 'style');
73+
for (var style in styles) {
74+
_resolveStyle(style, baseUri);
75+
}
76+
}
77+
78+
void _resolveStyle(StyleElement style, Uri baseUri) {
79+
style.text = resolveCssText(style.text, baseUri);
80+
}
81+
82+
String resolveCssText(String cssText, Uri baseUri) {
83+
cssText = _replaceUrlsInCssText(cssText, baseUri, cssUrlRegexp);
84+
return _replaceUrlsInCssText(cssText, baseUri, cssImportRegexp);
85+
}
86+
87+
void _resolveAttributes(Node root, Uri baseUri) {
88+
if (root is Element) {
89+
_resolveElementAttributes(root, baseUri);
90+
}
91+
92+
for (var node in _querySelectorAll(root, urlAttrsSelector)) {
93+
_resolveElementAttributes(node, baseUri);
94+
}
95+
}
96+
97+
void _resolveElementAttributes(Element element, Uri baseUri) {
98+
var attrs = element.attributes;
99+
for (var attr in urlAttrs) {
100+
if (attrs.containsKey(attr)) {
101+
var value = attrs[attr];
102+
if (!value.contains(urlTemplateSearch)) {
103+
attrs[attr] = combine(baseUri, value).toString();
104+
}
105+
}
106+
}
107+
}
108+
109+
String _replaceUrlsInCssText(String cssText, Uri baseUri, RegExp regexp) {
110+
return cssText.replaceAllMapped(regexp, (match) {
111+
var url = match[3].trim();
112+
var urlPath = combine(baseUri, url).toString();
113+
return '${match[1].trim()}${match[2]}${urlPath}${match[2]})';
114+
});
115+
}
116+
/// Combines a type-based URI with a relative URI.
117+
///
118+
/// [baseUri] is assumed to use package: syntax for package-relative
119+
/// URIs, while [uri] is assumed to use 'packages/' syntax for
120+
/// package-relative URIs. Resulting URIs will use 'packages/' to indicate
121+
/// package-relative URIs.
122+
String combine(Uri baseUri, String uri) {
123+
if (!_config.useRelativeUrls) {
124+
return uri;
125+
}
126+
127+
if (uri == null) {
128+
uri = baseUri.path;
129+
} else {
130+
// if it's absolute but not package-relative, then just use that
131+
// The "packages/" test is just for backward compatibility. It's ok to
132+
// not resolve them, even through they're relative URLs, because in a Dart
133+
// application, "packages/" is managed by pub which creates a symlinked
134+
// hierarchy and they should all resolve to the same file at any level
135+
// that a "packages/" exists.
136+
if (uri.startsWith("/") || uri.startsWith('packages/')) {
137+
return uri;
138+
}
139+
}
140+
// If it's not absolute, then resolve it first
141+
Uri resolved = baseUri.resolve(uri);
142+
143+
// If it's package-relative, tack on 'packages/' - Note that eventually
144+
// we may want to change this to be '/packages/' to make it truly absolute
145+
if (resolved.scheme == 'package') {
146+
return 'packages/${resolved.path}';
147+
} else if (resolved.isAbsolute && resolved.toString().startsWith(_baseUri)) {
148+
var path = resolved.path;
149+
return path.startsWith("/") ? path.substring(1) : path;
150+
} else {
151+
return resolved.toString();
152+
}
153+
}
154+
155+
String combineWithType(Type type, String uri) {
156+
if (_config.useRelativeUrls) {
157+
return combine(_uriMapper.uriForType(type), uri);
158+
} else {
159+
return uri;
160+
}
161+
}
162+
}
163+
164+
@Injectable()
165+
class ResourceResolverConfig {
166+
bool useRelativeUrls;
167+
168+
ResourceResolverConfig(): useRelativeUrls = true;
169+
170+
ResourceResolverConfig.resolveRelativeUrls(this.useRelativeUrls);
171+
}

lib/core_dom/shadow_dom_component_factory.dart

+28-8
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@ abstract class BoundComponentFactory {
1212
Function call(dom.Element element);
1313

1414
static async.Future<ViewFactory> _viewFuture(
15-
Component component, ViewCache viewCache, DirectiveMap directives) {
15+
Component component, ViewCache viewCache, DirectiveMap directives,
16+
TypeToUriMapper uriMapper, ResourceUrlResolver resourceResolver, Type type) {
17+
1618
if (component.template != null) {
17-
return new async.Future.value(viewCache.fromHtml(component.template, directives));
19+
// TODO(chirayu): Replace this line with
20+
// var baseUri = uriMapper.uriForType(type);
21+
// once we have removed _NullUriMapper.
22+
var baseUriString = resourceResolver.combineWithType(type, null);
23+
var baseUri = (baseUriString != null) ? Uri.parse(baseUriString) : null;
24+
return new async.Future.value(viewCache.fromHtml(component.template, directives, baseUri));
1825
}
1926
if (component.templateUrl != null) {
20-
return viewCache.fromUrl(component.templateUrl, directives);
27+
var url = resourceResolver.combineWithType(type, component.templateUrl);
28+
var baseUri = Uri.parse(url);
29+
return viewCache.fromUrl(url, directives, baseUri);
2130
}
2231
return null;
2332
}
@@ -43,12 +52,15 @@ class ShadowDomComponentFactory implements ComponentFactory {
4352
final dom.NodeTreeSanitizer treeSanitizer;
4453
final Expando expando;
4554
final CompilerConfig config;
55+
final TypeToUriMapper uriMapper;
56+
final ResourceUrlResolver resourceResolver;
4657

4758
final Map<_ComponentAssetKey, async.Future<dom.StyleElement>> styleElementCache = {};
4859

4960
ShadowDomComponentFactory(this.viewCache, this.http, this.templateCache, this.platform,
5061
this.componentCssRewriter, this.treeSanitizer, this.expando,
51-
this.config, CacheRegister cacheRegister) {
62+
this.config, this.uriMapper, this.resourceResolver,
63+
CacheRegister cacheRegister) {
5264
cacheRegister.registerCache("ShadowDomComponentFactoryStyles", styleElementCache);
5365
}
5466

@@ -76,10 +88,16 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
7688
_viewFuture = BoundComponentFactory._viewFuture(
7789
_component,
7890
new PlatformViewCache(_componentFactory.viewCache, _tag, _componentFactory.platform),
79-
_directives);
91+
_directives,
92+
_componentFactory.uriMapper,
93+
_componentFactory.resourceResolver,
94+
_ref.type);
8095
}
8196

82-
async.Future<dom.StyleElement> _styleFuture(cssUrl) {
97+
async.Future<dom.StyleElement> _styleFuture(cssUrl, {resolveUri: true}) {
98+
if (resolveUri)
99+
cssUrl = _componentFactory.resourceResolver.combineWithType(_ref.type, cssUrl);
100+
83101
Http http = _componentFactory.http;
84102
TemplateCache templateCache = _componentFactory.templateCache;
85103
WebPlatform platform = _componentFactory.platform;
@@ -89,7 +107,7 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
89107
return _componentFactory.styleElementCache.putIfAbsent(
90108
new _ComponentAssetKey(_tag, cssUrl), () =>
91109
http.get(cssUrl, cache: templateCache)
92-
.then((resp) => resp.responseText,
110+
.then((resp) => _componentFactory.resourceResolver.resolveCssText(resp.responseText, Uri.parse(cssUrl)),
93111
onError: (e) => '/*\n$e\n*/\n')
94112
.then((String css) {
95113

@@ -135,7 +153,9 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
135153

136154
async.Future<Iterable<dom.StyleElement>> cssFuture;
137155
if (_component.useNgBaseCss == true) {
138-
cssFuture = async.Future.wait([async.Future.wait(baseCss.urls.map(_styleFuture)), _styleElementsFuture]).then((twoLists) {
156+
cssFuture = async.Future.wait([async.Future.wait(baseCss.urls.map(
157+
(cssUrl) => _styleFuture(cssUrl, resolveUri: false))), _styleElementsFuture])
158+
.then((twoLists) {
139159
assert(twoLists.length == 2);return []
140160
..addAll(twoLists[0])
141161
..addAll(twoLists[1]);

lib/core_dom/transcluding_component_factory.dart

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ class TranscludingComponentFactory implements ComponentFactory {
66
final Expando expando;
77
final ViewCache viewCache;
88
final CompilerConfig config;
9+
final TypeToUriMapper uriMapper;
10+
final ResourceUrlResolver resourceResolver;
911

10-
TranscludingComponentFactory(this.expando, this.viewCache, this.config);
12+
TranscludingComponentFactory(this.expando, this.viewCache, this.config,
13+
this.uriMapper, this.resourceResolver);
1114

1215
bind(DirectiveRef ref, directives, injector) =>
1316
new BoundTranscludingComponentFactory(this, ref, directives, injector);
@@ -26,7 +29,10 @@ class BoundTranscludingComponentFactory implements BoundComponentFactory {
2629
_viewFuture = BoundComponentFactory._viewFuture(
2730
_component,
2831
_f.viewCache,
29-
_directives);
32+
_directives,
33+
_f.uriMapper,
34+
_f.resourceResolver,
35+
_ref.type);
3036
}
3137

3238
List<Key> get callArgs => _CALL_ARGS;

0 commit comments

Comments
 (0)