feat(StyleInliner): StyleInliner inlines @import css rules
This commit is contained in:
		
							parent
							
								
									e8bec99aa6
								
							
						
					
					
						commit
						e0cf1c7ab5
					
				
							
								
								
									
										115
									
								
								modules/angular2/src/core/compiler/style_inliner.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								modules/angular2/src/core/compiler/style_inliner.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | |||||||
|  | import {XHR} from 'angular2/src/core/compiler/xhr/xhr'; | ||||||
|  | 
 | ||||||
|  | import {ListWrapper} from 'angular2/src/facade/collection'; | ||||||
|  | import { | ||||||
|  |   isBlank, | ||||||
|  |   RegExp, | ||||||
|  |   RegExpWrapper, | ||||||
|  |   StringWrapper, | ||||||
|  |   normalizeBlank, | ||||||
|  | } from 'angular2/src/facade/lang'; | ||||||
|  | import { | ||||||
|  |   Promise, | ||||||
|  |   PromiseWrapper, | ||||||
|  | } from 'angular2/src/facade/async'; | ||||||
|  | 
 | ||||||
|  | export class StyleInliner { | ||||||
|  |   _xhr: XHR; | ||||||
|  | 
 | ||||||
|  |   constructor(xhr: XHR) { | ||||||
|  |     this._xhr = xhr; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // TODO(vicb): handle base url
 | ||||||
|  |   // TODO(vicb): Union types: returns either a Promise<string> or a string
 | ||||||
|  |   inlineImports(cssText: string) { | ||||||
|  |     return this._inlineImports(cssText, []); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _inlineImports(cssText: string, inlinedUrls: List<string>) { | ||||||
|  |     var partIndex = 0; | ||||||
|  |     var parts = StringWrapper.split(cssText, _importRe); | ||||||
|  | 
 | ||||||
|  |     if (parts.length === 1) { | ||||||
|  |       // no @import rule found, return the original css
 | ||||||
|  |       return cssText; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var promises = []; | ||||||
|  | 
 | ||||||
|  |     while (partIndex < parts.length - 1) { | ||||||
|  |       var prefix = parts[partIndex]; | ||||||
|  |       var rule = parts[partIndex + 1]; | ||||||
|  |       var url = _extractUrl(rule); | ||||||
|  |       var mediaQuery = _extractMediaQuery(rule); | ||||||
|  | 
 | ||||||
|  |       var promise; | ||||||
|  |       if (isBlank(url) || ListWrapper.contains(inlinedUrls, url)) { | ||||||
|  |         // The current import rule has already been inlined, return the prefix only
 | ||||||
|  |         // Importing again might cause a circular dependency
 | ||||||
|  |         promise = PromiseWrapper.resolve(prefix); | ||||||
|  |       } else { | ||||||
|  |         ListWrapper.push(inlinedUrls, url); | ||||||
|  |         promise = PromiseWrapper.then( | ||||||
|  |           this._xhr.get(url), | ||||||
|  |           (css) => { | ||||||
|  |             // resolve nested @import rules
 | ||||||
|  |             css = this._inlineImports(css, inlinedUrls); | ||||||
|  |             if (PromiseWrapper.isPromise(css)) { | ||||||
|  |               // wait until nested @import are inlined
 | ||||||
|  |               return css.then((css) => prefix + _wrapInMediaRule(css, mediaQuery)+ '\n') ; | ||||||
|  |             } else { | ||||||
|  |               // there are no nested @import, return the css
 | ||||||
|  |               return prefix + _wrapInMediaRule(css, mediaQuery) + '\n'; | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           (error) => `/* failed to import ${url} */\n` | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       ListWrapper.push(promises, promise); | ||||||
|  |       partIndex += 2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return PromiseWrapper.then( | ||||||
|  |       PromiseWrapper.all(promises), | ||||||
|  |       function (cssParts) { | ||||||
|  |         var cssText = cssParts.join(''); | ||||||
|  |         if (partIndex < parts.length) { | ||||||
|  |           // append whatever css located after the last @import rule
 | ||||||
|  |           cssText += parts[partIndex]; | ||||||
|  |         } | ||||||
|  |         return cssText; | ||||||
|  |       }, | ||||||
|  |       function(e) { | ||||||
|  |         throw 'error'; | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Extracts the url from an import rule, supported formats:
 | ||||||
|  | // - 'url' / "url",
 | ||||||
|  | // - url('url') / url("url")
 | ||||||
|  | function _extractUrl(importRule: string): string { | ||||||
|  |   var match = RegExpWrapper.firstMatch(_urlRe, importRule); | ||||||
|  |   if (isBlank(match)) return null; | ||||||
|  |   return match[1]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Extracts the media query from an import rule.
 | ||||||
|  | // Returns null when there is no media query.
 | ||||||
|  | function _extractMediaQuery(importRule: string): string { | ||||||
|  |   var match = RegExpWrapper.firstMatch(_mediaQueryRe, importRule); | ||||||
|  |   if (isBlank(match)) return null; | ||||||
|  |   var mediaQuery = match[1].trim(); | ||||||
|  |   return (mediaQuery.length > 0) ? mediaQuery: null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Wraps the css in a media rule when the media query is not null
 | ||||||
|  | function _wrapInMediaRule(css: string, query: string) { | ||||||
|  |   return (isBlank(query)) ? css : `@media ${query} {\n${css}\n}`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _importRe = RegExpWrapper.create('@import\\s+([^;]+);'); | ||||||
|  | var _urlRe = RegExpWrapper.create('(?:url\\(\\s*)?[\'"]([^\'"]+)[\'"]'); | ||||||
|  | var _mediaQueryRe = RegExpWrapper.create('[\'"][^\'"]+[\'"]\\s*\\)?\\s*(.*)'); | ||||||
							
								
								
									
										165
									
								
								modules/angular2/test/core/compiler/style_inliner_spec.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								modules/angular2/test/core/compiler/style_inliner_spec.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,165 @@ | |||||||
|  | import {describe, it, expect, beforeEach, ddescribe, iit, xit, el} from 'angular2/test_lib'; | ||||||
|  | import {StyleInliner} from 'angular2/src/core/compiler/style_inliner'; | ||||||
|  | 
 | ||||||
|  | import {isBlank} from 'angular2/src/facade/lang'; | ||||||
|  | import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; | ||||||
|  | import {Map, MapWrapper} from 'angular2/src/facade/collection'; | ||||||
|  | 
 | ||||||
|  | import {XHR} from 'angular2/src/core/compiler/xhr/xhr'; | ||||||
|  | 
 | ||||||
|  | export function main() { | ||||||
|  |   describe('StyleInliner', () => { | ||||||
|  |     describe('loading', () => { | ||||||
|  |       it('should return a string when there is no import statement', () => { | ||||||
|  |         var css = '.main {}'; | ||||||
|  |         var loader = new StyleInliner(null); | ||||||
|  |         var loadedCss = loader.inlineImports(css); | ||||||
|  |         expect(loadedCss).not.toBePromise(); | ||||||
|  |         expect(loadedCss).toEqual(css); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should inline @import rules', (done) => { | ||||||
|  |         var xhr = new FakeXHR(); | ||||||
|  |         xhr.reply('one.css', '.one {}'); | ||||||
|  |         var css = '@import "one.css";.main {}'; | ||||||
|  |         var loader = new StyleInliner(xhr); | ||||||
|  |         var loadedCss = loader.inlineImports(css); | ||||||
|  |         expect(loadedCss).toBePromise(); | ||||||
|  |         PromiseWrapper.then( | ||||||
|  |           loadedCss, | ||||||
|  |           function(css) { | ||||||
|  |             expect(css).toEqual('.one {}\n.main {}'); | ||||||
|  |             done(); | ||||||
|  |           }, | ||||||
|  |           function(e) { | ||||||
|  |             throw 'fail;' | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should handle @import error gracefuly', (done) => { | ||||||
|  |         var xhr = new FakeXHR(); | ||||||
|  |         var css = '@import "one.css";.main {}'; | ||||||
|  |         var loader = new StyleInliner(xhr); | ||||||
|  |         var loadedCss = loader.inlineImports(css); | ||||||
|  |         expect(loadedCss).toBePromise(); | ||||||
|  |         PromiseWrapper.then( | ||||||
|  |           loadedCss, | ||||||
|  |           function(css) { | ||||||
|  |             expect(css).toEqual('/* failed to import one.css */\n.main {}'); | ||||||
|  |             done(); | ||||||
|  |           }, | ||||||
|  |           function(e) { | ||||||
|  |             throw 'fail;' | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should inline multiple @import rules', (done) => { | ||||||
|  |         var xhr = new FakeXHR(); | ||||||
|  |         xhr.reply('one.css', '.one {}'); | ||||||
|  |         xhr.reply('two.css', '.two {}'); | ||||||
|  |         var css = '@import "one.css";@import "two.css";.main {}'; | ||||||
|  |         var loader = new StyleInliner(xhr); | ||||||
|  |         var loadedCss = loader.inlineImports(css); | ||||||
|  |         expect(loadedCss).toBePromise(); | ||||||
|  |         PromiseWrapper.then( | ||||||
|  |           loadedCss, | ||||||
|  |           function(css) { | ||||||
|  |             expect(css).toEqual('.one {}\n.two {}\n.main {}'); | ||||||
|  |             done(); | ||||||
|  |           }, | ||||||
|  |           function(e) { | ||||||
|  |             throw 'fail;' | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should inline nested @import rules', (done) => { | ||||||
|  |         var xhr = new FakeXHR(); | ||||||
|  |         xhr.reply('one.css', '@import "two.css";.one {}'); | ||||||
|  |         xhr.reply('two.css', '.two {}'); | ||||||
|  |         var css = '@import "one.css";.main {}'; | ||||||
|  |         var loader = new StyleInliner(xhr); | ||||||
|  |         var loadedCss = loader.inlineImports(css); | ||||||
|  |         expect(loadedCss).toBePromise(); | ||||||
|  |         PromiseWrapper.then( | ||||||
|  |           loadedCss, | ||||||
|  |           function(css) { | ||||||
|  |             expect(css).toEqual('.two {}\n.one {}\n.main {}'); | ||||||
|  |             done(); | ||||||
|  |           }, | ||||||
|  |           function(e) { | ||||||
|  |             throw 'fail;' | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       it('should handle circular dependencies gracefuly', (done) => { | ||||||
|  |         var xhr = new FakeXHR(); | ||||||
|  |         xhr.reply('one.css', '@import "two.css";.one {}'); | ||||||
|  |         xhr.reply('two.css', '@import "one.css";.two {}'); | ||||||
|  |         var css = '@import "one.css";.main {}'; | ||||||
|  |         var loader = new StyleInliner(xhr); | ||||||
|  |         var loadedCss = loader.inlineImports(css); | ||||||
|  |         expect(loadedCss).toBePromise(); | ||||||
|  |         PromiseWrapper.then( | ||||||
|  |           loadedCss, | ||||||
|  |           function(css) { | ||||||
|  |             expect(css).toEqual('.two {}\n.one {}\n.main {}'); | ||||||
|  |             done(); | ||||||
|  |           }, | ||||||
|  |           function(e) { | ||||||
|  |             throw 'fail;' | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('media query', () => { | ||||||
|  |       it('should wrap inlined content in media query', (done) => { | ||||||
|  |         var xhr = new FakeXHR(); | ||||||
|  |         xhr.reply('one.css', '.one {}'); | ||||||
|  |         var css = '@import "one.css" (min-width: 700px) and (orientation: landscape);'; | ||||||
|  |         var loader = new StyleInliner(xhr); | ||||||
|  |         var loadedCss = loader.inlineImports(css); | ||||||
|  |         expect(loadedCss).toBePromise(); | ||||||
|  |         PromiseWrapper.then( | ||||||
|  |           loadedCss, | ||||||
|  |           function(css) { | ||||||
|  |             expect(css).toEqual('@media (min-width: 700px) and (orientation: landscape) {\n.one {}\n}\n'); | ||||||
|  |             done(); | ||||||
|  |           }, | ||||||
|  |           function(e) { | ||||||
|  |             throw 'fail;' | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class FakeXHR extends XHR { | ||||||
|  |   _responses: Map; | ||||||
|  | 
 | ||||||
|  |   constructor() { | ||||||
|  |     super(); | ||||||
|  |     this._responses = MapWrapper.create(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get(url: string): Promise<string> { | ||||||
|  |     var response = MapWrapper.get(this._responses, url); | ||||||
|  |     if (isBlank(response)) { | ||||||
|  |       return PromiseWrapper.reject('xhr error'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return PromiseWrapper.resolve(response); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   reply(url: string, response: string) { | ||||||
|  |     MapWrapper.set(this._responses, url, response); | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user