feat(StyleInliner): StyleInliner inlines @import css rules
This commit is contained in:
parent
e8bec99aa6
commit
e0cf1c7ab5
|
@ -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*(.*)');
|
|
@ -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…
Reference in New Issue