diff --git a/modules/angular2/src/compiler/api.ts b/modules/angular2/src/compiler/api.ts index 0aa8fec0d9..0440e4e792 100644 --- a/modules/angular2/src/compiler/api.ts +++ b/modules/angular2/src/compiler/api.ts @@ -3,11 +3,13 @@ import {HtmlAst} from './html_ast'; import {ChangeDetectionStrategy} from 'angular2/src/core/change_detection/change_detection'; export class TypeMetadata { + id: number; type: any; typeName: string; typeUrl: string; - constructor({type, typeName, typeUrl}: - {type?: string, typeName?: string, typeUrl?: string} = {}) { + constructor({id, type, typeName, typeUrl}: + {id?: number, type?: string, typeName?: string, typeUrl?: string} = {}) { + this.id = id; this.type = type; this.typeName = typeName; this.typeUrl = typeUrl; @@ -65,11 +67,11 @@ export class TemplateMetadata { styleAbsUrls: string[]; ngContentSelectors: string[]; constructor({encapsulation, nodes, styles, styleAbsUrls, ngContentSelectors}: { - encapsulation: ViewEncapsulation, - nodes: HtmlAst[], - styles: string[], - styleAbsUrls: string[], - ngContentSelectors: string[] + encapsulation?: ViewEncapsulation, + nodes?: HtmlAst[], + styles?: string[], + styleAbsUrls?: string[], + ngContentSelectors?: string[] }) { this.encapsulation = encapsulation; this.nodes = nodes; @@ -121,3 +123,7 @@ export class DirectiveMetadata { this.template = template; } } + +export class SourceModule { + constructor(public moduleName: string, public source: string, public imports: string[][]) {} +} diff --git a/modules/angular2/src/compiler/style_compiler.ts b/modules/angular2/src/compiler/style_compiler.ts new file mode 100644 index 0000000000..50c035ab35 --- /dev/null +++ b/modules/angular2/src/compiler/style_compiler.ts @@ -0,0 +1,138 @@ +import {DirectiveMetadata, SourceModule, ViewEncapsulation} from './api'; +import {XHR} from 'angular2/src/core/render/xhr'; +import {StringWrapper, isJsObject, isBlank} from 'angular2/src/core/facade/lang'; +import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async'; +import {ShadowCss} from 'angular2/src/core/render/dom/compiler/shadow_css'; +import {UrlResolver} from 'angular2/src/core/services/url_resolver'; +import {resolveStyleUrls} from './style_url_resolver'; + +const COMPONENT_VARIABLE = '%COMP%'; +var COMPONENT_REGEX = /%COMP%/g; +const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; +const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`; +var ESCAPE_STRING_RE = /'|\\|\n/g; +var IS_DART = !isJsObject({}); + +export class StyleCompiler { + private _styleCache: Map> = new Map>(); + private _shadowCss: ShadowCss = new ShadowCss(); + + constructor(private _xhr: XHR, private _urlResolver: UrlResolver) {} + + compileComponentRuntime(component: DirectiveMetadata): Promise { + var styles = component.template.styles; + var styleAbsUrls = component.template.styleAbsUrls; + return this._loadStyles(styles, styleAbsUrls, + component.template.encapsulation === ViewEncapsulation.Emulated) + .then(styles => styles.map(style => StringWrapper.replaceAll(style, COMPONENT_REGEX, + `${component.type.id}`))); + } + + compileComponentCodeGen(component: DirectiveMetadata): SourceModule { + var shim = component.template.encapsulation === ViewEncapsulation.Emulated; + var suffix; + if (shim) { + var componentId = `${ component.type.id}`; + suffix = + codeGenMapArray(['style'], `style${codeGenReplaceAll(COMPONENT_VARIABLE, componentId)}`); + } else { + suffix = ''; + } + return this._styleCodeGen(`$component.type.typeUrl}.styles`, component.template.styles, + component.template.styleAbsUrls, shim, suffix); + } + + compileStylesheetCodeGen(moduleName: string, cssText: string): SourceModule[] { + var styleWithImports = resolveStyleUrls(this._urlResolver, moduleName, cssText); + return [ + this._styleCodeGen(moduleName, [styleWithImports.style], styleWithImports.styleUrls, false, + ''), + this._styleCodeGen(moduleName, [styleWithImports.style], styleWithImports.styleUrls, true, '') + ]; + } + + private _loadStyles(plainStyles: string[], absUrls: string[], + encapsulate: boolean): Promise { + var promises = absUrls.map((absUrl) => { + var cacheKey = `${absUrl}${encapsulate ? '.shim' : ''}`; + var result = this._styleCache.get(cacheKey); + if (isBlank(result)) { + result = this._xhr.get(absUrl).then((style) => { + var styleWithImports = resolveStyleUrls(this._urlResolver, absUrl, style); + return this._loadStyles([styleWithImports.style], styleWithImports.styleUrls, + encapsulate); + }); + this._styleCache.set(cacheKey, result); + } + return result; + }); + return PromiseWrapper.all(promises).then((nestedStyles: string[][]) => { + var result = plainStyles.map(plainStyle => this._shimIfNeeded(plainStyle, encapsulate)); + nestedStyles.forEach(styles => styles.forEach(style => result.push(style))); + return result; + }); + } + + private _styleCodeGen(moduleName: string, plainStyles: string[], absUrls: string[], shim: boolean, + suffix: string): SourceModule { + var imports: string[][] = []; + var moduleSource = `${codeGenExportVar('STYLES')} (`; + moduleSource += + `[${plainStyles.map( plainStyle => escapeString(this._shimIfNeeded(plainStyle, shim)) ).join(',')}]`; + for (var i = 0; i < absUrls.length; i++) { + var url = absUrls[i]; + var moduleAlias = `import${i}`; + imports.push([this._shimModuleName(url, shim), moduleAlias]); + moduleSource += `${codeGenConcatArray(moduleAlias+'.STYLES')}`; + } + moduleSource += `)${suffix};`; + return new SourceModule(this._shimModuleName(moduleName, shim), moduleSource, imports); + } + + private _shimIfNeeded(style: string, shim: boolean): string { + return shim ? this._shadowCss.shimCssText(style, CONTENT_ATTR, HOST_ATTR) : style; + } + + private _shimModuleName(originalUrl: string, shim: boolean): string { + return shim ? `${originalUrl}.shim` : originalUrl; + } +} + +function escapeString(input: string): string { + var escapedInput = StringWrapper.replaceAllMapped(input, ESCAPE_STRING_RE, (match) => { + if (match[0] == "'" || match[0] == '\\') { + return `\\${match[0]}`; + } else { + return '\\n'; + } + }); + return `'${escapedInput}'`; +} + +function codeGenExportVar(name: string): string { + if (IS_DART) { + return `var ${name} =`; + } else { + return `var ${name} = exports.${name} =`; + } +} + +function codeGenConcatArray(expression: string): string { + return `${IS_DART ? '..addAll' : '.concat'}(${expression})`; +} + +function codeGenMapArray(argNames: string[], callback: string): string { + if (IS_DART) { + return `.map( (${argNames.join(',')}) => ${callback} ).toList()`; + } else { + return `.map(function(${argNames.join(',')}) { return ${callback}; })`; + } +} + +function codeGenReplaceAll(pattern: string, value: string): string { + if (IS_DART) { + return `.replaceAll('${pattern}', '${value}')`; + } else { + return `.replace(/${pattern}/g, '${value}')`; + } +} \ No newline at end of file diff --git a/modules/angular2/src/compiler/style_url_resolver.ts b/modules/angular2/src/compiler/style_url_resolver.ts index d60069ba18..1448bef3c8 100644 --- a/modules/angular2/src/compiler/style_url_resolver.ts +++ b/modules/angular2/src/compiler/style_url_resolver.ts @@ -1,65 +1,52 @@ // Some of the code comes from WebComponents.JS // https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js -import {Injectable} from 'angular2/di'; -import {RegExp, RegExpWrapper, StringWrapper} from 'angular2/src/core/facade/lang'; +import {RegExp, RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/core/facade/lang'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; /** * Rewrites URLs by resolving '@import' and 'url()' URLs from the given base URL, * removes and returns the @import urls */ -@Injectable() -export class StyleUrlResolver { - constructor(public _resolver: UrlResolver) {} - - resolveUrls(cssText: string, baseUrl: string): string { - cssText = this._replaceUrls(cssText, _cssUrlRe, baseUrl); - return cssText; - } - - extractImports(cssText: string): StyleWithImports { - var foundUrls = []; - cssText = this._extractUrls(cssText, _cssImportRe, foundUrls); - return new StyleWithImports(cssText, foundUrls); - } - - _replaceUrls(cssText: string, re: RegExp, baseUrl: string) { - return StringWrapper.replaceAllMapped(cssText, re, (m) => { - var pre = m[1]; - var originalUrl = m[2]; - if (RegExpWrapper.test(_dataUrlRe, originalUrl)) { - // Do not attempt to resolve data: URLs - return m[0]; - } - var url = StringWrapper.replaceAll(originalUrl, _quoteRe, ''); - var post = m[3]; - - var resolvedUrl = this._resolver.resolve(baseUrl, url); - - return pre + "'" + resolvedUrl + "'" + post; - }); - } - - _extractUrls(cssText: string, re: RegExp, foundUrls: string[]) { - return StringWrapper.replaceAllMapped(cssText, re, (m) => { - var originalUrl = m[2]; - if (RegExpWrapper.test(_dataUrlRe, originalUrl)) { - // Do not attempt to resolve data: URLs - return m[0]; - } - var url = StringWrapper.replaceAll(originalUrl, _quoteRe, ''); - foundUrls.push(url); - return ''; - }); - } +export function resolveStyleUrls(resolver: UrlResolver, baseUrl: string, cssText: string): + StyleWithImports { + var foundUrls = []; + cssText = extractUrls(resolver, baseUrl, cssText, foundUrls); + cssText = replaceUrls(resolver, baseUrl, cssText); + return new StyleWithImports(cssText, foundUrls); } export class StyleWithImports { constructor(public style: string, public styleUrls: string[]) {} } +function extractUrls(resolver: UrlResolver, baseUrl: string, cssText: string, foundUrls: string[]): + string { + return StringWrapper.replaceAllMapped(cssText, _cssImportRe, (m) => { + var url = isPresent(m[1]) ? m[1] : m[2]; + foundUrls.push(resolver.resolve(baseUrl, url)); + return ''; + }); +} + +function replaceUrls(resolver: UrlResolver, baseUrl: string, cssText: string): string { + return StringWrapper.replaceAllMapped(cssText, _cssUrlRe, (m) => { + var pre = m[1]; + var originalUrl = m[2]; + if (RegExpWrapper.test(_dataUrlRe, originalUrl)) { + // Do not attempt to resolve data: URLs + return m[0]; + } + var url = StringWrapper.replaceAll(originalUrl, _quoteRe, ''); + var post = m[3]; + + var resolvedUrl = resolver.resolve(baseUrl, url); + + return pre + "'" + resolvedUrl + "'" + post; + }); +} + var _cssUrlRe = /(url\()([^)]*)(\))/g; -var _cssImportRe = /(@import[\s]+(?:url\()?)['"]?([^'"\)]*)['"]?(.*;)/g; +var _cssImportRe = /@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g; var _quoteRe = /['"]/g; var _dataUrlRe = /^['"]?data:/g; diff --git a/modules/angular2/src/compiler/template_loader.ts b/modules/angular2/src/compiler/template_loader.ts index 47f2a25684..d835d8e8d1 100644 --- a/modules/angular2/src/compiler/template_loader.ts +++ b/modules/angular2/src/compiler/template_loader.ts @@ -4,7 +4,7 @@ import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async'; import {XHR} from 'angular2/src/core/render/xhr'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; -import {StyleUrlResolver} from './style_url_resolver'; +import {resolveStyleUrls} from './style_url_resolver'; import { HtmlAstVisitor, @@ -26,7 +26,7 @@ const STYLE_ELEMENT = 'style'; export class TemplateLoader { constructor(private _xhr: XHR, private _urlResolver: UrlResolver, - private _styleUrlResolver: StyleUrlResolver, private _domParser: HtmlParser) {} + private _domParser: HtmlParser) {} loadTemplate(directiveType: TypeMetadata, encapsulation: ViewEncapsulation, template: string, templateUrl: string, styles: string[], @@ -51,14 +51,12 @@ export class TemplateLoader { var remainingNodes = htmlVisitAll(visitor, domNodes); var allStyles = styles.concat(visitor.styles); var allStyleUrls = styleUrls.concat(visitor.styleUrls); - allStyles = allStyles.map(style => { - var styleWithImports = this._styleUrlResolver.extractImports(style); + var allResolvedStyles = allStyles.map(style => { + var styleWithImports = resolveStyleUrls(this._urlResolver, templateSourceUrl, style); styleWithImports.styleUrls.forEach(styleUrl => allStyleUrls.push(styleUrl)); return styleWithImports.style; }); - var allResolvedStyles = - allStyles.map(style => this._styleUrlResolver.resolveUrls(style, templateSourceUrl)); var allStyleAbsUrls = allStyleUrls.map(styleUrl => this._urlResolver.resolve(templateSourceUrl, styleUrl)); return new TemplateMetadata({ diff --git a/modules/angular2/test/compiler/eval_module_spec.ts b/modules/angular2/test/compiler/eval_module_spec.ts index 040264d089..902d3386df 100644 --- a/modules/angular2/test/compiler/eval_module_spec.ts +++ b/modules/angular2/test/compiler/eval_module_spec.ts @@ -11,11 +11,9 @@ import { AsyncTestCompleter, inject } from 'angular2/test_lib'; -import {PromiseWrapper} from 'angular2/src/core/facade/async'; import {IS_DART} from '../platform'; import {evalModule} from './eval_module'; -import {SourceModule} from 'angular2/src/compiler/api'; // This export is used by this test code // when evaling the test module! @@ -23,14 +21,16 @@ export var TEST_VALUE = 23; export function main() { describe('evalModule', () => { - it('should call the "run" function and allow to use imports', inject([AsyncTestCompleter], (async) => { + it('should call the "run" function and allow to use imports', + inject([AsyncTestCompleter], (async) => { var moduleSource = IS_DART ? testDartModule : testJsModule; var imports = [['angular2/test/compiler/eval_module_spec', 'testMod']]; - - evalModule(moduleSource, imports, [1]).then( (value) => { - expect(value).toEqual([1, 23]); - async.done(); - }); + + evalModule(moduleSource, imports, [1]) + .then((value) => { + expect(value).toEqual([1, 23]); + async.done(); + }); })); }); } diff --git a/modules/angular2/test/compiler/style_compiler_import.shim.ts b/modules/angular2/test/compiler/style_compiler_import.shim.ts new file mode 100644 index 0000000000..08366cae76 --- /dev/null +++ b/modules/angular2/test/compiler/style_compiler_import.shim.ts @@ -0,0 +1,2 @@ +// used by style_compiler_spec.ts +export var STYLES = ['span[_ngcontent-%COMP%] {\ncolor: blue;\n}']; diff --git a/modules/angular2/test/compiler/style_compiler_import.ts b/modules/angular2/test/compiler/style_compiler_import.ts new file mode 100644 index 0000000000..e27607695f --- /dev/null +++ b/modules/angular2/test/compiler/style_compiler_import.ts @@ -0,0 +1,2 @@ +// used by style_compiler_spec.ts +export var STYLES = ['span {color: blue}']; diff --git a/modules/angular2/test/compiler/style_compiler_spec.ts b/modules/angular2/test/compiler/style_compiler_spec.ts new file mode 100644 index 0000000000..8fe6a23df1 --- /dev/null +++ b/modules/angular2/test/compiler/style_compiler_spec.ts @@ -0,0 +1,201 @@ +import { + ddescribe, + describe, + xdescribe, + it, + iit, + xit, + expect, + beforeEach, + afterEach, + AsyncTestCompleter, + inject +} from 'angular2/test_lib'; +import {IS_DART} from '../platform'; +import {SpyXHR} from '../core/spies'; + +import {CONST_EXPR, isPresent, BaseException} from 'angular2/src/core/facade/lang'; +import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async'; +import {evalModule} from './eval_module'; +import {StyleCompiler} from 'angular2/src/compiler/style_compiler'; +import {UrlResolver} from 'angular2/src/core/services/url_resolver'; +import { + DirectiveMetadata, + TemplateMetadata, + TypeMetadata, + ViewEncapsulation +} from 'angular2/src/compiler/api'; + +// Attention: These module names have to correspond to real modules! +const MODULE_NAME = 'angular2/test/compiler/style_compiler_spec'; +const IMPORT_ABS_MODULE_NAME = 'angular2/test/compiler/style_compiler_import'; +const IMPORT_REL_MODULE_NAME = './style_compiler_import'; +// Note: Not a real module, only used via mocks. +const IMPORT_ABS_MODULE_NAME_WITH_IMPORT = + 'angular2/test/compiler/style_compiler_transitive_import'; + +export function main() { + describe('StyleCompiler', () => { + var compiler: StyleCompiler; + var xhr; + + beforeEach(() => { + xhr = new SpyXHR(); + compiler = new StyleCompiler(xhr, new UrlResolver()); + }); + + function comp(styles: string[], styleAbsUrls: string[], encapsulation: ViewEncapsulation): + DirectiveMetadata { + return new DirectiveMetadata({ + type: new TypeMetadata({id: 23, typeUrl: 'someUrl'}), + template: new TemplateMetadata( + {styles: styles, styleAbsUrls: styleAbsUrls, encapsulation: encapsulation}) + }); + } + + describe('compileComponentRuntime', () => { + function runTest(styles: string[], styleAbsUrls: string[], encapsulation: ViewEncapsulation, + expectedStyles: string[]) { + return inject([AsyncTestCompleter], (async) => { + // Note: Can't use MockXHR as the xhr is called recursively, + // so we can't trigger flush. + xhr.spy('get').andCallFake((url) => { + var response; + if (url == IMPORT_ABS_MODULE_NAME) { + response = 'span {color: blue}'; + } else if (url == IMPORT_ABS_MODULE_NAME_WITH_IMPORT) { + response = `a {color: green}@import ${IMPORT_REL_MODULE_NAME};`; + } else { + throw new BaseException(`Unexpected url ${url}`); + } + return PromiseWrapper.resolve(response); + }); + compiler.compileComponentRuntime(comp(styles, styleAbsUrls, encapsulation)) + .then((value) => { + expect(value).toEqual(expectedStyles); + async.done(); + }); + }); + } + + describe('no shim', () => { + var encapsulation = ViewEncapsulation.None; + + it('should compile plain css rules', + runTest(['div {color: red}', 'span {color: blue}'], [], encapsulation, + ['div {color: red}', 'span {color: blue}'])); + + it('should allow to import rules', + runTest(['div {color: red}'], [IMPORT_ABS_MODULE_NAME], encapsulation, + ['div {color: red}', 'span {color: blue}'])); + + it('should allow to import rules transitively', + runTest(['div {color: red}'], [IMPORT_ABS_MODULE_NAME_WITH_IMPORT], encapsulation, + ['div {color: red}', 'a {color: green}', 'span {color: blue}'])); + }); + + describe('with shim', () => { + var encapsulation = ViewEncapsulation.Emulated; + + it('should compile plain css rules', + runTest( + ['div {\ncolor: red;\n}', 'span {\ncolor: blue;\n}'], [], encapsulation, + ['div[_ngcontent-23] {\ncolor: red;\n}', 'span[_ngcontent-23] {\ncolor: blue;\n}'])); + + it('should allow to import rules', + runTest( + ['div {\ncolor: red;\n}'], [IMPORT_ABS_MODULE_NAME], encapsulation, + ['div[_ngcontent-23] {\ncolor: red;\n}', 'span[_ngcontent-23] {\ncolor: blue;\n}'])); + + it('should allow to import rules transitively', + runTest(['div {\ncolor: red;\n}'], [IMPORT_ABS_MODULE_NAME_WITH_IMPORT], encapsulation, [ + 'div[_ngcontent-23] {\ncolor: red;\n}', + 'a[_ngcontent-23] {\ncolor: green;\n}', + 'span[_ngcontent-23] {\ncolor: blue;\n}' + ])); + }); + }); + + describe('compileComponentCodeGen', () => { + function runTest(styles: string[], styleAbsUrls: string[], encapsulation: ViewEncapsulation, + expectedStyles: string[]) { + return inject([AsyncTestCompleter], (async) => { + var sourceModule = + compiler.compileComponentCodeGen(comp(styles, styleAbsUrls, encapsulation)); + evalModule(testableModule(sourceModule.source), sourceModule.imports, null) + .then((value) => { + expect(value).toEqual(expectedStyles); + async.done(); + }); + }); + } + + describe('no shim', () => { + var encapsulation = ViewEncapsulation.None; + + it('should compile plain css ruless', + runTest(['div {color: red}', 'span {color: blue}'], [], encapsulation, + ['div {color: red}', 'span {color: blue}'])); + + it('should compile css rules with newlines and quotes', + runTest(['div\n{"color": \'red\'}'], [], encapsulation, ['div\n{"color": \'red\'}'])); + + it('should allow to import rules', + runTest(['div {color: red}'], [IMPORT_ABS_MODULE_NAME], encapsulation, + ['div {color: red}', 'span {color: blue}'])); + }); + + describe('with shim', () => { + var encapsulation = ViewEncapsulation.Emulated; + + it('should compile plain css ruless', + runTest( + ['div {\ncolor: red;\n}', 'span {\ncolor: blue;\n}'], [], encapsulation, + ['div[_ngcontent-23] {\ncolor: red;\n}', 'span[_ngcontent-23] {\ncolor: blue;\n}'])); + + it('should allow to import rules', + runTest( + ['div {color: red}'], [IMPORT_ABS_MODULE_NAME], encapsulation, + ['div[_ngcontent-23] {\ncolor: red;\n}', 'span[_ngcontent-23] {\ncolor: blue;\n}'])); + }); + }); + + describe('compileStylesheetCodeGen', () => { + function runTest(style: string, expectedStyles: string[], expectedShimStyles: string[]) { + return inject([AsyncTestCompleter], (async) => { + var sourceModules = compiler.compileStylesheetCodeGen(MODULE_NAME, style); + PromiseWrapper.all(sourceModules.map(sourceModule => + evalModule(testableModule(sourceModule.source), + sourceModule.imports, null))) + .then((values) => { + expect(values[0]).toEqual(expectedStyles); + expect(values[1]).toEqual(expectedShimStyles); + async.done(); + }); + }); + } + + it('should compile plain css rules', runTest('div {color: red;}', ['div {color: red;}'], + ['div[_ngcontent-%COMP%] {\ncolor: red;\n}'])); + + it('should allow to import rules with relative paths', + runTest(`div {color: red}@import ${IMPORT_REL_MODULE_NAME};`, + ['div {color: red}', 'span {color: blue}'], [ + 'div[_ngcontent-%COMP%] {\ncolor: red;\n}', + 'span[_ngcontent-%COMP%] {\ncolor: blue;\n}' + ])); + }); + }); +} + +function testableModule(sourceModule: string) { + if (IS_DART) { + return `${sourceModule} + run(_) { return STYLES; } +`; + } else { + return `${sourceModule} + exports.run = function(_) { return STYLES; }; +`; + } +} diff --git a/modules/angular2/test/compiler/style_url_resolver_spec.ts b/modules/angular2/test/compiler/style_url_resolver_spec.ts index 6b477fc28b..6d9a3dcb9a 100644 --- a/modules/angular2/test/compiler/style_url_resolver_spec.ts +++ b/modules/angular2/test/compiler/style_url_resolver_spec.ts @@ -1,87 +1,91 @@ import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/test_lib'; -import {StyleUrlResolver} from 'angular2/src/compiler/style_url_resolver'; +import {resolveStyleUrls} from 'angular2/src/compiler/style_url_resolver'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; export function main() { describe('StyleUrlResolver', () => { - let styleUrlResolver: StyleUrlResolver; + var urlResolver; - beforeEach(() => { styleUrlResolver = new StyleUrlResolver(new UrlResolver()); }); + beforeEach(() => { urlResolver = new UrlResolver(); }); - describe('resolveUrls', () => { - it('should resolve "url()" urls', () => { - var css = ` - .foo { - background-image: url("double.jpg"); - background-image: url('simple.jpg'); - background-image: url(noquote.jpg); - }`; - var expectedCss = ` - .foo { - background-image: url('http://ng.io/double.jpg'); - background-image: url('http://ng.io/simple.jpg'); - background-image: url('http://ng.io/noquote.jpg'); - }`; + it('should resolve "url()" urls', () => { + var css = ` + .foo { + background-image: url("double.jpg"); + background-image: url('simple.jpg'); + background-image: url(noquote.jpg); + }`; + var expectedCss = ` + .foo { + background-image: url('http://ng.io/double.jpg'); + background-image: url('http://ng.io/simple.jpg'); + background-image: url('http://ng.io/noquote.jpg'); + }`; - var resolvedCss = styleUrlResolver.resolveUrls(css, 'http://ng.io'); - expect(resolvedCss).toEqual(expectedCss); - }); - - it('should not strip quotes from inlined SVG styles', () => { - var css = ` - .selector { - background:rgb(55,71,79) url('data:image/svg+xml;utf8,'); - background:rgb(55,71,79) url("data:image/svg+xml;utf8,"); - background:rgb(55,71,79) url("/some/data:image"); - } - `; - - var expectedCss = ` - .selector { - background:rgb(55,71,79) url('data:image/svg+xml;utf8,'); - background:rgb(55,71,79) url("data:image/svg+xml;utf8,"); - background:rgb(55,71,79) url('http://ng.io/some/data:image'); - } - `; - - var resolvedCss = styleUrlResolver.resolveUrls(css, 'http://ng.io'); - expect(resolvedCss).toEqual(expectedCss); - }); + var resolvedCss = resolveStyleUrls(urlResolver, 'http://ng.io', css).style; + expect(resolvedCss).toEqual(expectedCss); }); - describe('extractUrls', () => { - it('should extract "@import" urls', () => { - var css = ` - @import '1.css'; - @import "2.css"; - `; - var styleWithImports = styleUrlResolver.extractImports(css); - expect(styleWithImports.style.trim()).toEqual(''); - expect(styleWithImports.styleUrls).toEqual(['1.css', '2.css']); - }); + it('should not strip quotes from inlined SVG styles', () => { + var css = ` + .selector { + background:rgb(55,71,79) url('data:image/svg+xml;utf8,'); + background:rgb(55,71,79) url("data:image/svg+xml;utf8,"); + background:rgb(55,71,79) url("/some/data:image"); + } + `; - it('should extract "@import url()" urls', () => { - var css = ` - @import url('3.css'); - @import url("4.css"); - @import url(5.css); - `; - var styleWithImports = styleUrlResolver.extractImports(css); - expect(styleWithImports.style.trim()).toEqual(''); - expect(styleWithImports.styleUrls).toEqual(['3.css', '4.css', '5.css']); - }); + var expectedCss = ` + .selector { + background:rgb(55,71,79) url('data:image/svg+xml;utf8,'); + background:rgb(55,71,79) url("data:image/svg+xml;utf8,"); + background:rgb(55,71,79) url('http://ng.io/some/data:image'); + } + `; - it('should extract media query in "@import"', () => { - var css = ` - @import 'print.css' print; - @import url(print2.css) print; - `; - var styleWithImports = styleUrlResolver.extractImports(css); - expect(styleWithImports.style.trim()).toEqual(''); - expect(styleWithImports.styleUrls).toEqual(['print.css', 'print2.css']); - }); + var resolvedCss = resolveStyleUrls(urlResolver, 'http://ng.io', css).style; + expect(resolvedCss).toEqual(expectedCss); + }); + it('should extract "@import" urls', () => { + var css = ` + @import '1.css'; + @import "2.css"; + `; + var styleWithImports = resolveStyleUrls(urlResolver, 'http://ng.io', css); + expect(styleWithImports.style.trim()).toEqual(''); + expect(styleWithImports.styleUrls).toEqual(['http://ng.io/1.css', 'http://ng.io/2.css']); + }); + + it('should extract "@import url()" urls', () => { + var css = ` + @import url('3.css'); + @import url("4.css"); + @import url(5.css); + `; + var styleWithImports = resolveStyleUrls(urlResolver, 'http://ng.io', css); + expect(styleWithImports.style.trim()).toEqual(''); + expect(styleWithImports.styleUrls) + .toEqual(['http://ng.io/3.css', 'http://ng.io/4.css', 'http://ng.io/5.css']); + }); + + it('should extract "@import urls and keep rules in the same line', () => { + var css = `@import url('some.css');div {color: red};`; + var styleWithImports = resolveStyleUrls(urlResolver, 'http://ng.io', css); + expect(styleWithImports.style.trim()).toEqual('div {color: red};'); + expect(styleWithImports.styleUrls).toEqual(['http://ng.io/some.css']); + }); + + it('should extract media query in "@import"', () => { + var css = ` + @import 'print1.css' print; + @import url(print2.css) print; + `; + var styleWithImports = resolveStyleUrls(urlResolver, 'http://ng.io', css); + expect(styleWithImports.style.trim()).toEqual(''); + expect(styleWithImports.styleUrls) + .toEqual(['http://ng.io/print1.css', 'http://ng.io/print2.css']); }); }); diff --git a/modules/angular2/test/compiler/template_loader_spec.ts b/modules/angular2/test/compiler/template_loader_spec.ts index 47bba55abf..11bb454040 100644 --- a/modules/angular2/test/compiler/template_loader_spec.ts +++ b/modules/angular2/test/compiler/template_loader_spec.ts @@ -19,7 +19,6 @@ import {TypeMetadata, ViewEncapsulation, TemplateMetadata} from 'angular2/src/co import {TemplateLoader} from 'angular2/src/compiler/template_loader'; import {UrlResolver} from 'angular2/src/core/services/url_resolver'; -import {StyleUrlResolver} from 'angular2/src/compiler/style_url_resolver'; import {humanizeDom} from './html_parser_spec'; import {HtmlTextAst, HtmlElementAst, HtmlAttrAst} from 'angular2/src/compiler/html_ast'; import {XHR} from 'angular2/src/core/render/xhr'; @@ -33,8 +32,7 @@ export function main() { beforeEach(inject([XHR], (mockXhr) => { xhr = mockXhr; var urlResolver = new UrlResolver(); - loader = - new TemplateLoader(xhr, urlResolver, new StyleUrlResolver(urlResolver), new HtmlParser()); + loader = new TemplateLoader(xhr, urlResolver, new HtmlParser()); dirType = new TypeMetadata({typeUrl: 'http://sometypeurl', typeName: 'SomeComp'}); })); diff --git a/modules/angular2/test/core/spies.dart b/modules/angular2/test/core/spies.dart index a85dd3bd19..d21e41b337 100644 --- a/modules/angular2/test/core/spies.dart +++ b/modules/angular2/test/core/spies.dart @@ -12,6 +12,7 @@ import 'package:angular2/src/core/compiler/view_listener.dart'; import 'package:angular2/src/core/compiler/element_injector.dart'; import 'package:angular2/src/core/dom/dom_adapter.dart'; import 'package:angular2/test_lib.dart'; +import 'package:angular2/src/core/render/xhr.dart'; @proxy class SpyDependencyProvider extends SpyObject implements DependencyProvider { @@ -103,3 +104,8 @@ class SpyPreBuiltObjects extends SpyObject implements PreBuiltObjects { class SpyDomAdapter extends SpyObject implements DomAdapter { noSuchMethod(m) => super.noSuchMethod(m); } + +@proxy +class SpyXHR extends SpyObject implements XHR { + noSuchMethod(m) => super.noSuchMethod(m); +} diff --git a/modules/angular2/test/core/spies.ts b/modules/angular2/test/core/spies.ts index 8de1208889..e1e1867b7c 100644 --- a/modules/angular2/test/core/spies.ts +++ b/modules/angular2/test/core/spies.ts @@ -16,6 +16,7 @@ import {AppViewPool} from 'angular2/src/core/compiler/view_pool'; import {AppViewListener} from 'angular2/src/core/compiler/view_listener'; import {DomAdapter} from 'angular2/src/core/dom/dom_adapter'; import {ClientMessageBroker} from 'angular2/src/web_workers/shared/client_message_broker'; +import {XHR} from 'angular2/src/core/render/xhr'; import { ElementInjector, @@ -87,4 +88,8 @@ export class SpyPreBuiltObjects extends SpyObject { export class SpyDomAdapter extends SpyObject { constructor() { super(DomAdapter); } +} + +export class SpyXHR extends SpyObject { + constructor() { super(XHR); } } \ No newline at end of file diff --git a/test-main.dart b/test-main.dart index aca624b5c7..e187ab4f00 100644 --- a/test-main.dart +++ b/test-main.dart @@ -6,7 +6,7 @@ import 'package:angular2/src/test_lib/test_lib.dart' show testSetup; main() { unit.filterStacks = true; unit.formatStacks = false; - unit.unittestConfiguration.timeout = new Duration(milliseconds: 1000); + unit.unittestConfiguration.timeout = new Duration(milliseconds: 2000); _printWarnings();