Skip to content

Commit

Permalink
feat(urls): support relative CSS / template URLs in components
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Diana Salsbury authored and dsalsbury committed Aug 25, 2014
1 parent 600113a commit 6a06fb1
Show file tree
Hide file tree
Showing 37 changed files with 1,327 additions and 163 deletions.
4 changes: 4 additions & 0 deletions lib/application_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import 'package:angular/change_detection/change_detection.dart';
import 'package:angular/change_detection/dirty_checking_change_detector_dynamic.dart';
import 'package:angular/core/registry_dynamic.dart';
import 'package:angular/core/parser/dynamic_closure_map.dart';
import 'package:angular/core_dom/type_to_uri_mapper.dart';
import 'package:angular/core_dom/type_to_uri_mapper_dynamic.dart';
import 'dart:html';

/**
Expand All @@ -42,6 +44,7 @@ import 'dart:html';
'angular.core.parser.Parser',
'angular.core.parser.dynamic_parser',
'angular.core.parser.lexer',
'angular.core_dom.type_to_uri_mapper_dynamic',
'angular.core_dynamic.DynamicMetadataExtractor',
'perf_api',
List,
Expand All @@ -59,6 +62,7 @@ import 'dart:mirrors' show MirrorsUsed;
class _DynamicApplication extends Application {
_DynamicApplication() {
ngModule
..bind(TypeToUriMapper, toImplementation: DynamicTypeToUriMapper)
..bind(MetadataExtractor, toImplementation: DynamicMetadataExtractor)
..bind(FieldGetterFactory, toImplementation: DynamicFieldGetterFactory)
..bind(ClosureMap, toImplementation: DynamicClosureMap);
Expand Down
24 changes: 21 additions & 3 deletions lib/application_factory_static.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import 'package:angular/core/registry.dart';
import 'package:angular/core/parser/parser.dart';
import 'package:angular/core/parser/static_closure_map.dart';
import 'package:angular/core/parser/dynamic_parser.dart';
import 'package:angular/core_dom/type_to_uri_mapper.dart';
import 'package:angular/core/registry_static.dart';
import 'package:angular/change_detection/change_detection.dart';
import 'package:angular/change_detection/dirty_checking_change_detector_static.dart';
Expand All @@ -48,8 +49,10 @@ class _StaticApplication extends Application {
Map<Type, Object> metadata,
Map<String, FieldGetter> fieldGetters,
Map<String, FieldSetter> fieldSetters,
Map<String, Symbol> symbols) {
Map<String, Symbol> symbols,
TypeToUriMapper uriMapper) {
ngModule
..bind(TypeToUriMapper, toValue: uriMapper)
..bind(MetadataExtractor, toValue: new StaticMetadataExtractor(metadata))
..bind(FieldGetterFactory, toValue: new StaticFieldGetterFactory(fieldGetters))
..bind(ClosureMap, toValue: new StaticClosureMap(fieldGetters, fieldSetters, symbols));
Expand Down Expand Up @@ -83,10 +86,25 @@ class _StaticApplication extends Application {
* .run();
*
*/

class _NullUriMapper extends TypeToUriMapper {
Uri uriForType(Type type) {
throw "You did not pass in a TypeToUriMapper to your StaticApplicationFactory."
"(This would have been automatic if you used Dart transformers.) You must pass"
"in a valid TypeTpUriMapper when constructing your Static Application";
}
}
Application staticApplicationFactory(
Map<Type, Object> metadata,
Map<String, FieldGetter> fieldGetters,
Map<String, FieldSetter> fieldSetters,
Map<String, Symbol> symbols) {
return new _StaticApplication(metadata, fieldGetters, fieldSetters, symbols);
Map<String, Symbol> symbols,
[TypeToUriMapper uriMapper]) {
// TODO: uriMapper is optional for backwards compatibility. Make it a
// required parameter by the next release.
if (uriMapper == null) {
uriMapper = new _NullUriMapper();
}
return new _StaticApplication(metadata, fieldGetters, fieldSetters,
symbols, uriMapper);
}
2 changes: 2 additions & 0 deletions lib/core/module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export "package:angular/core_dom/module_internal.dart" show
RequestInterceptor,
Response,
ResponseError,
ResourceResolverConfig,
ResourceUrlResolver,
UrlRewriter,
TemplateCache,
View,
Expand Down
8 changes: 8 additions & 0 deletions lib/core_dom/module_internal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import 'package:angular/core_dom/static_keys.dart';
import 'package:angular/core_dom/directive_injector.dart';
export 'package:angular/core_dom/directive_injector.dart' show DirectiveInjector;

import 'package:angular/core_dom/type_to_uri_mapper.dart';
import 'package:angular/core_dom/resource_url_resolver.dart';
export 'package:angular/core_dom/resource_url_resolver.dart'
show ResourceUrlResolver, ResourceResolverConfig;

import 'package:angular/change_detection/watch_group.dart' show Watch, PrototypeMap;
import 'package:angular/change_detection/ast_parser.dart';
import 'package:angular/core/registry.dart';
Expand Down Expand Up @@ -77,13 +82,16 @@ class CoreDomModule extends Module {
bind(ComponentCssRewriter);
bind(WebPlatform);

bind(ResourceUrlResolver);
bind(Http);
bind(UrlRewriter);
bind(HttpBackend);
bind(HttpDefaultHeaders);
bind(HttpDefaults);
bind(HttpInterceptors);
bind(HttpConfig);
bind(ResourceResolverConfig, toValue:
new ResourceResolverConfig(useRelativeUrls: false));
bind(Animate);
bind(ViewCache);
bind(BrowserCookies);
Expand Down
168 changes: 168 additions & 0 deletions lib/core_dom/resource_url_resolver.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Dart port of
* https://github.com/Polymer/platform-dev/blob/896e245a0046a397bfc0190d958d2bd162e8f53c/src/url.js
*
* This converts URIs within a document from relative URIs to being absolute URIs.
*/

library angular.core_dom.absolute_uris;

import 'dart:html';
import 'dart:js' as js;

import 'package:di/di.dart';

import 'package:angular/core_dom/type_to_uri_mapper.dart';

@Injectable()
class ResourceUrlResolver {
static final RegExp cssUrlRegexp = new RegExp(r'''(\burl\((?:[\s]+)?)(['"]?)([^]*)(\2(?:[\s]+)?\))''');
static final RegExp cssImportRegexp = new RegExp(r'(@import[\s]+(?!url\())([^;]*)(;)');
static const List<String> urlAttrs = const ['href', 'src', 'action'];
static final String urlAttrsSelector = '[${urlAttrs.join('],[')}]';
static final RegExp urlTemplateSearch = new RegExp('{{.*}}');
static final RegExp quotes = new RegExp("[\"\']");

// Ensures that Uri.base is http/https.
final _baseUri = Uri.base.origin + ("/");

final TypeToUriMapper _uriMapper;
final ResourceResolverConfig _config;

ResourceUrlResolver(this._uriMapper, this._config);

String resolveHtml(String html, [Uri baseUri]) {
if (baseUri == null) {
return html;
}
HtmlDocument document = new DomParser().parseFromString(
"<!doctype html><html><body>$html</body></html>", "text/html");
_resolveDom(document.body, baseUri);
return document.body.innerHtml;
}

/**
* Resolves all relative URIs within the DOM from being relative to
* [originalBase] to being absolute.
*/
void _resolveDom(Node root, Uri baseUri) {
_resolveAttributes(root, baseUri);
_resolveStyles(root, baseUri);

// handle template.content
for (var template in _querySelectorAll(root, 'template')) {
if (template.content != null) {
_resolveDom(template.content, baseUri);
}
}
}

Iterable<Element> _querySelectorAll(Node node, String selectors) {
if (node is DocumentFragment) {
return node.querySelectorAll(selectors);
}
if (node is Element) {
return node.querySelectorAll(selectors);
}
return const [];
}

void _resolveStyles(Node node, Uri baseUri) {
var styles = _querySelectorAll(node, 'style');
for (var style in styles) {
_resolveStyle(style, baseUri);
}
}

void _resolveStyle(StyleElement style, Uri baseUri) {
style.text = resolveCssText(style.text, baseUri);
}

String resolveCssText(String cssText, Uri baseUri) {
cssText = _replaceUrlsInCssText(cssText, baseUri, cssUrlRegexp);
return _replaceUrlsInCssText(cssText, baseUri, cssImportRegexp);
}

void _resolveAttributes(Node root, Uri baseUri) {
if (root is Element) {
_resolveElementAttributes(root, baseUri);
}

for (var node in _querySelectorAll(root, urlAttrsSelector)) {
_resolveElementAttributes(node, baseUri);
}
}

void _resolveElementAttributes(Element element, Uri baseUri) {
var attrs = element.attributes;
for (var attr in urlAttrs) {
if (attrs.containsKey(attr)) {
var value = attrs[attr];
if (!value.contains(urlTemplateSearch)) {
attrs[attr] = combine(baseUri, value).toString();
}
}
}
}

String _replaceUrlsInCssText(String cssText, Uri baseUri, RegExp regexp) {
return cssText.replaceAllMapped(regexp, (match) {
var url = match[3].trim();
var urlPath = combine(baseUri, url).toString();
return '${match[1].trim()}${match[2]}${urlPath}${match[2]})';
});
}
/// Combines a type-based URI with a relative URI.
///
/// [baseUri] is assumed to use package: syntax for package-relative
/// URIs, while [uri] is assumed to use 'packages/' syntax for
/// package-relative URIs. Resulting URIs will use 'packages/' to indicate
/// package-relative URIs.
String combine(Uri baseUri, String uri) {
if (!_config.useRelativeUrls) {
return uri;
}

if (uri == null) {
uri = baseUri.path;
} else {
// if it's absolute but not package-relative, then just use that
// The "packages/" test is just for backward compatibility. It's ok to
// not resolve them, even through they're relative URLs, because in a Dart
// application, "packages/" is managed by pub which creates a symlinked
// hierarchy and they should all resolve to the same file at any level
// that a "packages/" exists.
if (uri.startsWith("/") || uri.startsWith('packages/')) {
return uri;
}
}
// If it's not absolute, then resolve it first
Uri resolved = baseUri.resolve(uri);

// If it's package-relative, tack on 'packages/' - Note that eventually
// we may want to change this to be '/packages/' to make it truly absolute
if (resolved.scheme == 'package') {
return 'packages/${resolved.path}';
} else if (resolved.isAbsolute && resolved.toString().startsWith(_baseUri)) {
var path = resolved.path;
return path.startsWith("/") ? path.substring(1) : path;
} else {
return resolved.toString();
}
}

String combineWithType(Type type, String uri) {
if (_config.useRelativeUrls) {
return combine(_uriMapper.uriForType(type), uri);
} else {
return uri;
}
}
}

@Injectable()
class ResourceResolverConfig {
bool useRelativeUrls;

ResourceResolverConfig({this.useRelativeUrls});
}
37 changes: 28 additions & 9 deletions lib/core_dom/shadow_dom_component_factory.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
part of angular.core.dom_internal;

abstract class ComponentFactory {
BoundComponentFactory bind(DirectiveRef ref, directives, Injector injector);
}
Expand All @@ -12,12 +12,20 @@ abstract class BoundComponentFactory {
Function call(dom.Element element);

static async.Future<ViewFactory> _viewFuture(
Component component, ViewCache viewCache, DirectiveMap directives) {
Component component, ViewCache viewCache, DirectiveMap directives,
TypeToUriMapper uriMapper, ResourceUrlResolver resourceResolver, Type type) {
if (component.template != null) {
return new async.Future.value(viewCache.fromHtml(component.template, directives));
// TODO: Replace this line with
// var baseUri = uriMapper.uriForType(type);
// once we have removed _NullUriMapper.
//var baseUri = resourceResolver.combineWithType(type, null);
var baseUri = uriMapper.uriForType(type);
return new async.Future.value(viewCache.fromHtml(component.template, directives, baseUri));
}
if (component.templateUrl != null) {
return viewCache.fromUrl(component.templateUrl, directives);
var url = resourceResolver.combineWithType(type, component.templateUrl);
var baseUri = Uri.parse(url);
return viewCache.fromUrl(url, directives, baseUri);
}
return null;
}
Expand All @@ -43,12 +51,15 @@ class ShadowDomComponentFactory implements ComponentFactory {
final dom.NodeTreeSanitizer treeSanitizer;
final Expando expando;
final CompilerConfig config;
final TypeToUriMapper uriMapper;
final ResourceUrlResolver resourceResolver;

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

ShadowDomComponentFactory(this.viewCache, this.http, this.templateCache, this.platform,
this.componentCssRewriter, this.treeSanitizer, this.expando,
this.config, CacheRegister cacheRegister) {
this.config, this.uriMapper, this.resourceResolver,
CacheRegister cacheRegister) {
cacheRegister.registerCache("ShadowDomComponentFactoryStyles", styleElementCache);
}

Expand Down Expand Up @@ -77,10 +88,16 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
_viewFuture = BoundComponentFactory._viewFuture(
_component,
new PlatformViewCache(_componentFactory.viewCache, _tag, _componentFactory.platform),
_directives);
_directives,
_componentFactory.uriMapper,
_componentFactory.resourceResolver,
_ref.type);
}

async.Future<dom.StyleElement> _styleFuture(cssUrl) {
async.Future<dom.StyleElement> _styleFuture(cssUrl, {resolveUri: true}) {
if (resolveUri)
cssUrl = _componentFactory.resourceResolver.combineWithType(_ref.type, cssUrl);

Http http = _componentFactory.http;
TemplateCache templateCache = _componentFactory.templateCache;
WebPlatform platform = _componentFactory.platform;
Expand All @@ -90,7 +107,7 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {
return _componentFactory.styleElementCache.putIfAbsent(
new _ComponentAssetKey(_tag, cssUrl), () =>
http.get(cssUrl, cache: templateCache)
.then((resp) => resp.responseText,
.then((resp) => _componentFactory.resourceResolver.resolveCssText(resp.responseText, Uri.parse(cssUrl)),
onError: (e) => '/*\n$e\n*/\n')
.then((String css) {

Expand Down Expand Up @@ -136,7 +153,9 @@ class BoundShadowDomComponentFactory implements BoundComponentFactory {

async.Future<Iterable<dom.StyleElement>> cssFuture;
if (_component.useNgBaseCss == true) {
cssFuture = async.Future.wait([async.Future.wait(baseCss.urls.map(_styleFuture)), _styleElementsFuture]).then((twoLists) {
cssFuture = async.Future.wait([async.Future.wait(baseCss.urls.map(
(cssUrl) => _styleFuture(cssUrl, resolveUri: false))), _styleElementsFuture])
.then((twoLists) {
assert(twoLists.length == 2);return []
..addAll(twoLists[0])
..addAll(twoLists[1]);
Expand Down
10 changes: 8 additions & 2 deletions lib/core_dom/transcluding_component_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ class TranscludingComponentFactory implements ComponentFactory {
final Expando expando;
final ViewCache viewCache;
final CompilerConfig config;
final TypeToUriMapper uriMapper;
final ResourceUrlResolver resourceResolver;

TranscludingComponentFactory(this.expando, this.viewCache, this.config);
TranscludingComponentFactory(this.expando, this.viewCache, this.config,
this.uriMapper, this.resourceResolver);

bind(DirectiveRef ref, directives, injector) =>
new BoundTranscludingComponentFactory(this, ref, directives, injector);
Expand All @@ -94,7 +97,10 @@ class BoundTranscludingComponentFactory implements BoundComponentFactory {
_viewFuture = BoundComponentFactory._viewFuture(
_component,
_f.viewCache,
_directives);
_directives,
_f.uriMapper,
_f.resourceResolver,
_ref.type);
}

List<Key> get callArgs => _CALL_ARGS;
Expand Down
Loading

0 comments on commit 6a06fb1

Please sign in to comment.