Skip to content

Commit 2384082

Browse files
committed
feat(compiler): add stylesheet compiler
Part of angular#3605 Closes angular#3891
1 parent 2a126f7 commit 2384082

13 files changed

+486
-139
lines changed

modules/angular2/src/compiler/api.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import {HtmlAst} from './html_ast';
33
import {ChangeDetectionStrategy} from 'angular2/src/core/change_detection/change_detection';
44

55
export class TypeMetadata {
6+
id: number;
67
type: any;
78
typeName: string;
89
typeUrl: string;
9-
constructor({type, typeName, typeUrl}:
10-
{type?: string, typeName?: string, typeUrl?: string} = {}) {
10+
constructor({id, type, typeName, typeUrl}:
11+
{id?: number, type?: string, typeName?: string, typeUrl?: string} = {}) {
12+
this.id = id;
1113
this.type = type;
1214
this.typeName = typeName;
1315
this.typeUrl = typeUrl;
@@ -65,11 +67,11 @@ export class TemplateMetadata {
6567
styleAbsUrls: string[];
6668
ngContentSelectors: string[];
6769
constructor({encapsulation, nodes, styles, styleAbsUrls, ngContentSelectors}: {
68-
encapsulation: ViewEncapsulation,
69-
nodes: HtmlAst[],
70-
styles: string[],
71-
styleAbsUrls: string[],
72-
ngContentSelectors: string[]
70+
encapsulation?: ViewEncapsulation,
71+
nodes?: HtmlAst[],
72+
styles?: string[],
73+
styleAbsUrls?: string[],
74+
ngContentSelectors?: string[]
7375
}) {
7476
this.encapsulation = encapsulation;
7577
this.nodes = nodes;
@@ -121,3 +123,7 @@ export class DirectiveMetadata {
121123
this.template = template;
122124
}
123125
}
126+
127+
export class SourceModule {
128+
constructor(public moduleName: string, public source: string, public imports: string[][]) {}
129+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {DirectiveMetadata, SourceModule, ViewEncapsulation} from './api';
2+
import {XHR} from 'angular2/src/core/render/xhr';
3+
import {StringWrapper, isJsObject, isBlank} from 'angular2/src/core/facade/lang';
4+
import {PromiseWrapper, Promise} from 'angular2/src/core/facade/async';
5+
import {ShadowCss} from 'angular2/src/core/render/dom/compiler/shadow_css';
6+
import {UrlResolver} from 'angular2/src/core/services/url_resolver';
7+
import {resolveStyleUrls} from './style_url_resolver';
8+
9+
const COMPONENT_VARIABLE = '%COMP%';
10+
var COMPONENT_REGEX = /%COMP%/g;
11+
const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`;
12+
const CONTENT_ATTR = `_ngcontent-${COMPONENT_VARIABLE}`;
13+
var ESCAPE_STRING_RE = /'|\\|\n/g;
14+
var IS_DART = !isJsObject({});
15+
16+
export class StyleCompiler {
17+
private _styleCache: Map<string, Promise<string[]>> = new Map<string, Promise<string[]>>();
18+
private _shadowCss: ShadowCss = new ShadowCss();
19+
20+
constructor(private _xhr: XHR, private _urlResolver: UrlResolver) {}
21+
22+
compileComponentRuntime(component: DirectiveMetadata): Promise<string[]> {
23+
var styles = component.template.styles;
24+
var styleAbsUrls = component.template.styleAbsUrls;
25+
return this._loadStyles(styles, styleAbsUrls,
26+
component.template.encapsulation === ViewEncapsulation.Emulated)
27+
.then(styles => styles.map(style => StringWrapper.replaceAll(style, COMPONENT_REGEX,
28+
`${component.type.id}`)));
29+
}
30+
31+
compileComponentCodeGen(component: DirectiveMetadata): SourceModule {
32+
var shim = component.template.encapsulation === ViewEncapsulation.Emulated;
33+
var suffix;
34+
if (shim) {
35+
var componentId = `${ component.type.id}`;
36+
suffix =
37+
codeGenMapArray(['style'], `style${codeGenReplaceAll(COMPONENT_VARIABLE, componentId)}`);
38+
} else {
39+
suffix = '';
40+
}
41+
return this._styleCodeGen(`$component.type.typeUrl}.styles`, component.template.styles,
42+
component.template.styleAbsUrls, shim, suffix);
43+
}
44+
45+
compileStylesheetCodeGen(moduleName: string, cssText: string): SourceModule[] {
46+
var styleWithImports = resolveStyleUrls(this._urlResolver, moduleName, cssText);
47+
return [
48+
this._styleCodeGen(moduleName, [styleWithImports.style], styleWithImports.styleUrls, false,
49+
''),
50+
this._styleCodeGen(moduleName, [styleWithImports.style], styleWithImports.styleUrls, true, '')
51+
];
52+
}
53+
54+
private _loadStyles(plainStyles: string[], absUrls: string[],
55+
encapsulate: boolean): Promise<string[]> {
56+
var promises = absUrls.map((absUrl) => {
57+
var cacheKey = `${absUrl}${encapsulate ? '.shim' : ''}`;
58+
var result = this._styleCache.get(cacheKey);
59+
if (isBlank(result)) {
60+
result = this._xhr.get(absUrl).then((style) => {
61+
var styleWithImports = resolveStyleUrls(this._urlResolver, absUrl, style);
62+
return this._loadStyles([styleWithImports.style], styleWithImports.styleUrls,
63+
encapsulate);
64+
});
65+
this._styleCache.set(cacheKey, result);
66+
}
67+
return result;
68+
});
69+
return PromiseWrapper.all(promises).then((nestedStyles: string[][]) => {
70+
var result = plainStyles.map(plainStyle => this._shimIfNeeded(plainStyle, encapsulate));
71+
nestedStyles.forEach(styles => styles.forEach(style => result.push(style)));
72+
return result;
73+
});
74+
}
75+
76+
private _styleCodeGen(moduleName: string, plainStyles: string[], absUrls: string[], shim: boolean,
77+
suffix: string): SourceModule {
78+
var imports: string[][] = [];
79+
var moduleSource = `${codeGenExportVar('STYLES')} (`;
80+
moduleSource +=
81+
`[${plainStyles.map( plainStyle => escapeString(this._shimIfNeeded(plainStyle, shim)) ).join(',')}]`;
82+
for (var i = 0; i < absUrls.length; i++) {
83+
var url = absUrls[i];
84+
var moduleAlias = `import${i}`;
85+
imports.push([this._shimModuleName(url, shim), moduleAlias]);
86+
moduleSource += `${codeGenConcatArray(moduleAlias+'.STYLES')}`;
87+
}
88+
moduleSource += `)${suffix};`;
89+
return new SourceModule(this._shimModuleName(moduleName, shim), moduleSource, imports);
90+
}
91+
92+
private _shimIfNeeded(style: string, shim: boolean): string {
93+
return shim ? this._shadowCss.shimCssText(style, CONTENT_ATTR, HOST_ATTR) : style;
94+
}
95+
96+
private _shimModuleName(originalUrl: string, shim: boolean): string {
97+
return shim ? `${originalUrl}.shim` : originalUrl;
98+
}
99+
}
100+
101+
function escapeString(input: string): string {
102+
var escapedInput = StringWrapper.replaceAllMapped(input, ESCAPE_STRING_RE, (match) => {
103+
if (match[0] == "'" || match[0] == '\\') {
104+
return `\\${match[0]}`;
105+
} else {
106+
return '\\n';
107+
}
108+
});
109+
return `'${escapedInput}'`;
110+
}
111+
112+
function codeGenExportVar(name: string): string {
113+
if (IS_DART) {
114+
return `var ${name} =`;
115+
} else {
116+
return `var ${name} = exports.${name} =`;
117+
}
118+
}
119+
120+
function codeGenConcatArray(expression: string): string {
121+
return `${IS_DART ? '..addAll' : '.concat'}(${expression})`;
122+
}
123+
124+
function codeGenMapArray(argNames: string[], callback: string): string {
125+
if (IS_DART) {
126+
return `.map( (${argNames.join(',')}) => ${callback} ).toList()`;
127+
} else {
128+
return `.map(function(${argNames.join(',')}) { return ${callback}; })`;
129+
}
130+
}
131+
132+
function codeGenReplaceAll(pattern: string, value: string): string {
133+
if (IS_DART) {
134+
return `.replaceAll('${pattern}', '${value}')`;
135+
} else {
136+
return `.replace(/${pattern}/g, '${value}')`;
137+
}
138+
}
Lines changed: 34 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,52 @@
11
// Some of the code comes from WebComponents.JS
22
// https://github.com/webcomponents/webcomponentsjs/blob/master/src/HTMLImports/path.js
33

4-
import {Injectable} from 'angular2/di';
5-
import {RegExp, RegExpWrapper, StringWrapper} from 'angular2/src/core/facade/lang';
4+
import {RegExp, RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/core/facade/lang';
65
import {UrlResolver} from 'angular2/src/core/services/url_resolver';
76

87
/**
98
* Rewrites URLs by resolving '@import' and 'url()' URLs from the given base URL,
109
* removes and returns the @import urls
1110
*/
12-
@Injectable()
13-
export class StyleUrlResolver {
14-
constructor(public _resolver: UrlResolver) {}
15-
16-
resolveUrls(cssText: string, baseUrl: string): string {
17-
cssText = this._replaceUrls(cssText, _cssUrlRe, baseUrl);
18-
return cssText;
19-
}
20-
21-
extractImports(cssText: string): StyleWithImports {
22-
var foundUrls = [];
23-
cssText = this._extractUrls(cssText, _cssImportRe, foundUrls);
24-
return new StyleWithImports(cssText, foundUrls);
25-
}
26-
27-
_replaceUrls(cssText: string, re: RegExp, baseUrl: string) {
28-
return StringWrapper.replaceAllMapped(cssText, re, (m) => {
29-
var pre = m[1];
30-
var originalUrl = m[2];
31-
if (RegExpWrapper.test(_dataUrlRe, originalUrl)) {
32-
// Do not attempt to resolve data: URLs
33-
return m[0];
34-
}
35-
var url = StringWrapper.replaceAll(originalUrl, _quoteRe, '');
36-
var post = m[3];
37-
38-
var resolvedUrl = this._resolver.resolve(baseUrl, url);
39-
40-
return pre + "'" + resolvedUrl + "'" + post;
41-
});
42-
}
43-
44-
_extractUrls(cssText: string, re: RegExp, foundUrls: string[]) {
45-
return StringWrapper.replaceAllMapped(cssText, re, (m) => {
46-
var originalUrl = m[2];
47-
if (RegExpWrapper.test(_dataUrlRe, originalUrl)) {
48-
// Do not attempt to resolve data: URLs
49-
return m[0];
50-
}
51-
var url = StringWrapper.replaceAll(originalUrl, _quoteRe, '');
52-
foundUrls.push(url);
53-
return '';
54-
});
55-
}
11+
export function resolveStyleUrls(resolver: UrlResolver, baseUrl: string, cssText: string):
12+
StyleWithImports {
13+
var foundUrls = [];
14+
cssText = extractUrls(resolver, baseUrl, cssText, foundUrls);
15+
cssText = replaceUrls(resolver, baseUrl, cssText);
16+
return new StyleWithImports(cssText, foundUrls);
5617
}
5718

5819
export class StyleWithImports {
5920
constructor(public style: string, public styleUrls: string[]) {}
6021
}
6122

23+
function extractUrls(resolver: UrlResolver, baseUrl: string, cssText: string, foundUrls: string[]):
24+
string {
25+
return StringWrapper.replaceAllMapped(cssText, _cssImportRe, (m) => {
26+
var url = isPresent(m[1]) ? m[1] : m[2];
27+
foundUrls.push(resolver.resolve(baseUrl, url));
28+
return '';
29+
});
30+
}
31+
32+
function replaceUrls(resolver: UrlResolver, baseUrl: string, cssText: string): string {
33+
return StringWrapper.replaceAllMapped(cssText, _cssUrlRe, (m) => {
34+
var pre = m[1];
35+
var originalUrl = m[2];
36+
if (RegExpWrapper.test(_dataUrlRe, originalUrl)) {
37+
// Do not attempt to resolve data: URLs
38+
return m[0];
39+
}
40+
var url = StringWrapper.replaceAll(originalUrl, _quoteRe, '');
41+
var post = m[3];
42+
43+
var resolvedUrl = resolver.resolve(baseUrl, url);
44+
45+
return pre + "'" + resolvedUrl + "'" + post;
46+
});
47+
}
48+
6249
var _cssUrlRe = /(url\()([^)]*)(\))/g;
63-
var _cssImportRe = /(@import[\s]+(?:url\()?)['"]?([^'"\)]*)['"]?(.*;)/g;
50+
var _cssImportRe = /@import\s+(?:url\()?\s*(?:(?:['"]([^'"]*))|([^;\)\s]*))[^;]*;?/g;
6451
var _quoteRe = /['"]/g;
6552
var _dataUrlRe = /^['"]?data:/g;

modules/angular2/src/compiler/template_loader.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async';
44

55
import {XHR} from 'angular2/src/core/render/xhr';
66
import {UrlResolver} from 'angular2/src/core/services/url_resolver';
7-
import {StyleUrlResolver} from './style_url_resolver';
7+
import {resolveStyleUrls} from './style_url_resolver';
88

99
import {
1010
HtmlAstVisitor,
@@ -26,7 +26,7 @@ const STYLE_ELEMENT = 'style';
2626

2727
export class TemplateLoader {
2828
constructor(private _xhr: XHR, private _urlResolver: UrlResolver,
29-
private _styleUrlResolver: StyleUrlResolver, private _domParser: HtmlParser) {}
29+
private _domParser: HtmlParser) {}
3030

3131
loadTemplate(directiveType: TypeMetadata, encapsulation: ViewEncapsulation, template: string,
3232
templateUrl: string, styles: string[],
@@ -51,14 +51,12 @@ export class TemplateLoader {
5151
var remainingNodes = htmlVisitAll(visitor, domNodes);
5252
var allStyles = styles.concat(visitor.styles);
5353
var allStyleUrls = styleUrls.concat(visitor.styleUrls);
54-
allStyles = allStyles.map(style => {
55-
var styleWithImports = this._styleUrlResolver.extractImports(style);
54+
var allResolvedStyles = allStyles.map(style => {
55+
var styleWithImports = resolveStyleUrls(this._urlResolver, templateSourceUrl, style);
5656
styleWithImports.styleUrls.forEach(styleUrl => allStyleUrls.push(styleUrl));
5757
return styleWithImports.style;
5858
});
5959

60-
var allResolvedStyles =
61-
allStyles.map(style => this._styleUrlResolver.resolveUrls(style, templateSourceUrl));
6260
var allStyleAbsUrls =
6361
allStyleUrls.map(styleUrl => this._urlResolver.resolve(templateSourceUrl, styleUrl));
6462
return new TemplateMetadata({

modules/angular2/test/compiler/eval_module_spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,26 @@ import {
1111
AsyncTestCompleter,
1212
inject
1313
} from 'angular2/test_lib';
14-
import {PromiseWrapper} from 'angular2/src/core/facade/async';
1514
import {IS_DART} from '../platform';
1615

1716
import {evalModule} from './eval_module';
18-
import {SourceModule} from 'angular2/src/compiler/api';
1917

2018
// This export is used by this test code
2119
// when evaling the test module!
2220
export var TEST_VALUE = 23;
2321

2422
export function main() {
2523
describe('evalModule', () => {
26-
it('should call the "run" function and allow to use imports', inject([AsyncTestCompleter], (async) => {
24+
it('should call the "run" function and allow to use imports',
25+
inject([AsyncTestCompleter], (async) => {
2726
var moduleSource = IS_DART ? testDartModule : testJsModule;
2827
var imports = [['angular2/test/compiler/eval_module_spec', 'testMod']];
29-
30-
evalModule(moduleSource, imports, [1]).then( (value) => {
31-
expect(value).toEqual([1, 23]);
32-
async.done();
33-
});
28+
29+
evalModule(moduleSource, imports, [1])
30+
.then((value) => {
31+
expect(value).toEqual([1, 23]);
32+
async.done();
33+
});
3434
}));
3535
});
3636
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// used by style_compiler_spec.ts
2+
export var STYLES = ['span[_ngcontent-%COMP%] {\ncolor: blue;\n}'];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// used by style_compiler_spec.ts
2+
export var STYLES = ['span {color: blue}'];

0 commit comments

Comments
 (0)