Skip to content

Commit

Permalink
feat(compiler): add codegen for stylesheets
Browse files Browse the repository at this point in the history
Part of angular#3605
  • Loading branch information
tbosch committed Sep 2, 2015
1 parent 7ce863b commit 796a9be
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 122 deletions.
4 changes: 4 additions & 0 deletions modules/angular2/src/compiler/api.ts
Expand Up @@ -98,3 +98,7 @@ export class DirectiveMetadata {
this.template = template;
}
}

export class SourceModule {
constructor(public source:string, public imports:string[][]) {}
}
25 changes: 25 additions & 0 deletions modules/angular2/src/compiler/style_codegen.ts
@@ -0,0 +1,25 @@
import {StringWrapper, isJsObject} from 'angular2/src/core/facade/lang';
import {SourceModule} from './api';

var ESCAPE_QUOTES_RE = /'|\\|\n/g;

export function styleCodeGen(cssText:string, importUrls:string[]):SourceModule {
var imports:string[][] = [];
var sourceParts:string[] = [];
for (var i=0; i<importUrls.length; i++) {
var url = importUrls[i];
var moduleAlias = `import${i}`;
imports.push([url, moduleAlias]);
sourceParts.push(`${moduleAlias}.STYLES`);
}
var escapedStyle = StringWrapper.replaceAllMapped(cssText, ESCAPE_QUOTES_RE, (match) => {
if (match[0] == "'" || match[0] == '\\') {
return `\\${match[0]}`
} else {
return '\\n';
}
});
sourceParts.push(`'${escapedStyle}'`);
var moduleSource = `${isJsObject(true) ? 'export': ''} var STYLES = ${sourceParts.join('+')};`;
return new SourceModule(moduleSource, imports);
}
78 changes: 32 additions & 46 deletions modules/angular2/src/compiler/style_url_resolver.ts
Expand Up @@ -2,64 +2,50 @@
// 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[]) {
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) {
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;
10 changes: 4 additions & 6 deletions modules/angular2/src/compiler/template_loader.ts
Expand Up @@ -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,
Expand All @@ -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[],
Expand All @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion modules/angular2/test/compiler/eval_module.dart
Expand Up @@ -12,7 +12,7 @@ createIsolateSource(String moduleSource, List<List<String>> moduleImports) {
moduleImports.forEach((sourceImport) {
String modName = sourceImport[0];
String modAlias = sourceImport[1];
moduleSourceParts.add("import 'package:${modName}.dart' as ${modAlias};");
moduleSourceParts.add("import 'package:${modName}.dart' as ${modAlias};");
});
moduleSourceParts.add(moduleSource);

Expand Down
64 changes: 64 additions & 0 deletions modules/angular2/test/compiler/style_codegen_spec.ts
@@ -0,0 +1,64 @@
import {
ddescribe,
describe,
xdescribe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
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 {styleCodeGen} from 'angular2/src/compiler/style_codegen';
import {SourceModule} from 'angular2/src/compiler/api';

// used by the test that imports styles from another stylesheet
export var STYLES = 'span {color: blue};';

const MODULE_NAME = 'angular2/test/compiler/style_codegen_spec';

export function main() {
describe('StyleCodeGen', () => {
it('should convert simple css rules', inject([AsyncTestCompleter], (async) => {
var sourceModule = styleCodeGen('div {color: red};', []);
evalModule(testableModule(sourceModule.source), sourceModule.imports, null).then( (value) => {
expect(value).toEqual('div {color: red};');
async.done();
});
}));

it('should convert css rules with newlines and quotes', inject([AsyncTestCompleter], (async) => {
var sourceModule = styleCodeGen('div\n{"color": \'red\'};', []);
evalModule(testableModule(sourceModule.source), sourceModule.imports, null).then( (value) => {
expect(value).toEqual('div\n{"color": \'red\'};');
async.done();
});
}));

it('should allow to import rules', inject([AsyncTestCompleter], (async) => {
var sourceModule = styleCodeGen('div {color: red};', [MODULE_NAME]);
evalModule(testableModule(sourceModule.source), sourceModule.imports, null).then( (value) => {
expect(value).toEqual('span {color: blue};div {color: red};');
async.done();
});
}));
});
}

function testableModule(sourceModule:string) {
if (IS_DART) {
return `${sourceModule}
run(_) { return STYLES; }
`;
} else {
return `${sourceModule}
exports.run = function(_) { return STYLES; };
`;
}
}
136 changes: 69 additions & 67 deletions modules/angular2/test/compiler/style_url_resolver_spec.ts
@@ -1,87 +1,89 @@
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);
});
var resolvedCss = resolveStyleUrls(urlResolver, 'http://ng.io', css).style;
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,<?xml version="1.0"?>');
background:rgb(55,71,79) url("data:image/svg+xml;utf8,<?xml version='1.0'?>");
background:rgb(55,71,79) url("/some/data:image");
}
`;
it('should not strip quotes from inlined SVG styles', () => {
var css = `
.selector {
background:rgb(55,71,79) url('data:image/svg+xml;utf8,<?xml version="1.0"?>');
background:rgb(55,71,79) url("data:image/svg+xml;utf8,<?xml version='1.0'?>");
background:rgb(55,71,79) url("/some/data:image");
}
`;

var expectedCss = `
.selector {
background:rgb(55,71,79) url('data:image/svg+xml;utf8,<?xml version="1.0"?>');
background:rgb(55,71,79) url("data:image/svg+xml;utf8,<?xml version='1.0'?>");
background:rgb(55,71,79) url('http://ng.io/some/data:image');
}
`;
var expectedCss = `
.selector {
background:rgb(55,71,79) url('data:image/svg+xml;utf8,<?xml version="1.0"?>');
background:rgb(55,71,79) url("data:image/svg+xml;utf8,<?xml version='1.0'?>");
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 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 = styleUrlResolver.extractImports(css);
expect(styleWithImports.style.trim()).toEqual('');
expect(styleWithImports.styleUrls).toEqual(['3.css', '4.css', '5.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 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']);
});
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']);
});

});
Expand Down

0 comments on commit 796a9be

Please sign in to comment.