Skip to content

Commit 6a06fb1

Browse files
Diana Salsburydsalsbury
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'! This feature is defaulted to an "off" state through the addition of the ResourceResolverConfig class. To turn the feature on, bind a new ResourceResolverConfig in your module: module.bind(ResourceResolverConfig, toValue: new ResourceResolverConfig(useRelativeUrls: true)); *Testing*: - e2e transformer tests can now be done on the sample application found in the test_transformers folder
1 parent 600113a commit 6a06fb1

37 files changed

+1327
-163
lines changed

lib/application_factory.dart

Lines changed: 4 additions & 0 deletions
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.dynamic_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,
@@ -59,6 +62,7 @@ import 'dart:mirrors' show MirrorsUsed;
5962
class _DynamicApplication extends Application {
6063
_DynamicApplication() {
6164
ngModule
65+
..bind(TypeToUriMapper, toImplementation: DynamicTypeToUriMapper)
6266
..bind(MetadataExtractor, toImplementation: DynamicMetadataExtractor)
6367
..bind(FieldGetterFactory, toImplementation: DynamicFieldGetterFactory)
6468
..bind(ClosureMap, toImplementation: DynamicClosureMap);

lib/application_factory_static.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import 'package:angular/core/registry.dart';
3535
import 'package:angular/core/parser/parser.dart';
3636
import 'package:angular/core/parser/static_closure_map.dart';
3737
import 'package:angular/core/parser/dynamic_parser.dart';
38+
import 'package:angular/core_dom/type_to_uri_mapper.dart';
3839
import 'package:angular/core/registry_static.dart';
3940
import 'package:angular/change_detection/change_detection.dart';
4041
import 'package:angular/change_detection/dirty_checking_change_detector_static.dart';
@@ -48,8 +49,10 @@ class _StaticApplication extends Application {
4849
Map<Type, Object> metadata,
4950
Map<String, FieldGetter> fieldGetters,
5051
Map<String, FieldSetter> fieldSetters,
51-
Map<String, Symbol> symbols) {
52+
Map<String, Symbol> symbols,
53+
TypeToUriMapper uriMapper) {
5254
ngModule
55+
..bind(TypeToUriMapper, toValue: uriMapper)
5356
..bind(MetadataExtractor, toValue: new StaticMetadataExtractor(metadata))
5457
..bind(FieldGetterFactory, toValue: new StaticFieldGetterFactory(fieldGetters))
5558
..bind(ClosureMap, toValue: new StaticClosureMap(fieldGetters, fieldSetters, symbols));
@@ -83,10 +86,25 @@ class _StaticApplication extends Application {
8386
* .run();
8487
*
8588
*/
89+
90+
class _NullUriMapper extends TypeToUriMapper {
91+
Uri uriForType(Type type) {
92+
throw "You did not pass in a TypeToUriMapper to your StaticApplicationFactory."
93+
"(This would have been automatic if you used Dart transformers.) You must pass"
94+
"in a valid TypeTpUriMapper when constructing your Static Application";
95+
}
96+
}
8697
Application staticApplicationFactory(
8798
Map<Type, Object> metadata,
8899
Map<String, FieldGetter> fieldGetters,
89100
Map<String, FieldSetter> fieldSetters,
90-
Map<String, Symbol> symbols) {
91-
return new _StaticApplication(metadata, fieldGetters, fieldSetters, symbols);
101+
Map<String, Symbol> symbols,
102+
[TypeToUriMapper uriMapper]) {
103+
// TODO: uriMapper is optional for backwards compatibility. Make it a
104+
// required parameter by the next release.
105+
if (uriMapper == null) {
106+
uriMapper = new _NullUriMapper();
107+
}
108+
return new _StaticApplication(metadata, fieldGetters, fieldSetters,
109+
symbols, uriMapper);
92110
}

lib/core/module.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export "package:angular/core_dom/module_internal.dart" show
5959
RequestInterceptor,
6060
Response,
6161
ResponseError,
62+
ResourceResolverConfig,
63+
ResourceUrlResolver,
6264
UrlRewriter,
6365
TemplateCache,
6466
View,

lib/core_dom/module_internal.dart

Lines changed: 8 additions & 0 deletions
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';
@@ -77,13 +82,16 @@ class CoreDomModule extends Module {
7782
bind(ComponentCssRewriter);
7883
bind(WebPlatform);
7984

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

lib/core_dom/shadow_dom_component_factory.dart

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
part of angular.core.dom_internal;
2-
2+
33
abstract class ComponentFactory {
44
BoundComponentFactory bind(DirectiveRef ref, directives, Injector injector);
55
}
@@ -12,12 +12,20 @@ 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) {
1617
if (component.template != null) {
17-
return new async.Future.value(viewCache.fromHtml(component.template, directives));
18+
// TODO: Replace this line with
19+
// var baseUri = uriMapper.uriForType(type);
20+
// once we have removed _NullUriMapper.
21+
//var baseUri = resourceResolver.combineWithType(type, null);
22+
var baseUri = uriMapper.uriForType(type);
23+
return new async.Future.value(viewCache.fromHtml(component.template, directives, baseUri));
1824
}
1925
if (component.templateUrl != null) {
20-
return viewCache.fromUrl(component.templateUrl, directives);
26+
var url = resourceResolver.combineWithType(type, component.templateUrl);
27+
var baseUri = Uri.parse(url);
28+
return viewCache.fromUrl(url, directives, baseUri);
2129
}
2230
return null;
2331
}
@@ -43,12 +51,15 @@ class ShadowDomComponentFactory implements ComponentFactory {
4351
final dom.NodeTreeSanitizer treeSanitizer;
4452
final Expando expando;
4553
final CompilerConfig config;
54+
final TypeToUriMapper uriMapper;
55+
final ResourceUrlResolver resourceResolver;
4656

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

4959
ShadowDomComponentFactory(this.viewCache, this.http, this.templateCache, this.platform,
5060
this.componentCssRewriter, this.treeSanitizer, this.expando,
51-
this.config, CacheRegister cacheRegister) {
61+
this.config, this.uriMapper, this.resourceResolver,
62+
CacheRegister cacheRegister) {
5263
cacheRegister.registerCache("ShadowDomComponentFactoryStyles", styleElementCache);
5364
}
5465

@@ -77,10 +88,16 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
7788
_viewFuture = BoundComponentFactory._viewFuture(
7889
_component,
7990
new PlatformViewCache(_componentFactory.viewCache, _tag, _componentFactory.platform),
80-
_directives);
91+
_directives,
92+
_componentFactory.uriMapper,
93+
_componentFactory.resourceResolver,
94+
_ref.type);
8195
}
8296

83-
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+
84101
Http http = _componentFactory.http;
85102
TemplateCache templateCache = _componentFactory.templateCache;
86103
WebPlatform platform = _componentFactory.platform;
@@ -90,7 +107,7 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
90107
return _componentFactory.styleElementCache.putIfAbsent(
91108
new _ComponentAssetKey(_tag, cssUrl), () =>
92109
http.get(cssUrl, cache: templateCache)
93-
.then((resp) => resp.responseText,
110+
.then((resp) => _componentFactory.resourceResolver.resolveCssText(resp.responseText, Uri.parse(cssUrl)),
94111
onError: (e) => '/*\n$e\n*/\n')
95112
.then((String css) {
96113

@@ -136,7 +153,9 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
136153

137154
async.Future<Iterable<dom.StyleElement>> cssFuture;
138155
if (_component.useNgBaseCss == true) {
139-
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) {
140159
assert(twoLists.length == 2);return []
141160
..addAll(twoLists[0])
142161
..addAll(twoLists[1]);

lib/core_dom/transcluding_component_factory.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,11 @@ class TranscludingComponentFactory implements ComponentFactory {
7474
final Expando expando;
7575
final ViewCache viewCache;
7676
final CompilerConfig config;
77+
final TypeToUriMapper uriMapper;
78+
final ResourceUrlResolver resourceResolver;
7779

78-
TranscludingComponentFactory(this.expando, this.viewCache, this.config);
80+
TranscludingComponentFactory(this.expando, this.viewCache, this.config,
81+
this.uriMapper, this.resourceResolver);
7982

8083
bind(DirectiveRef ref, directives, injector) =>
8184
new BoundTranscludingComponentFactory(this, ref, directives, injector);
@@ -94,7 +97,10 @@ class BoundTranscludingComponentFactory implements BoundComponentFactory {
9497
_viewFuture = BoundComponentFactory._viewFuture(
9598
_component,
9699
_f.viewCache,
97-
_directives);
100+
_directives,
101+
_f.uriMapper,
102+
_f.resourceResolver,
103+
_ref.type);
98104
}
99105

100106
List<Key> get callArgs => _CALL_ARGS;

0 commit comments

Comments
 (0)