Skip to content

Commit

Permalink
feat(compiler): add stylesheet compiler
Browse files Browse the repository at this point in the history
Part of #3605
Closes #3891
  • Loading branch information
tbosch committed Sep 4, 2015
1 parent 2a126f7 commit 2384082
Show file tree
Hide file tree
Showing 13 changed files with 486 additions and 139 deletions.
20 changes: 13 additions & 7 deletions modules/angular2/src/compiler/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -121,3 +123,7 @@ export class DirectiveMetadata {
this.template = template;
}
}

export class SourceModule {
constructor(public moduleName: string, public source: string, public imports: string[][]) {}
}
138 changes: 138 additions & 0 deletions modules/angular2/src/compiler/style_compiler.ts
Original file line number Diff line number Diff line change
@@ -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<string, Promise<string[]>> = new Map<string, Promise<string[]>>();
private _shadowCss: ShadowCss = new ShadowCss();

constructor(private _xhr: XHR, private _urlResolver: UrlResolver) {}

compileComponentRuntime(component: DirectiveMetadata): Promise<string[]> {
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<string[]> {
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}')`;
}
}
81 changes: 34 additions & 47 deletions modules/angular2/src/compiler/style_url_resolver.ts
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 4 additions & 6 deletions modules/angular2/src/compiler/template_loader.ts
Original file line number Diff line number Diff line change
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
16 changes: 8 additions & 8 deletions modules/angular2/test/compiler/eval_module_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ 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!
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();
});
}));
});
}
Expand Down
2 changes: 2 additions & 0 deletions modules/angular2/test/compiler/style_compiler_import.shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// used by style_compiler_spec.ts
export var STYLES = ['span[_ngcontent-%COMP%] {\ncolor: blue;\n}'];
2 changes: 2 additions & 0 deletions modules/angular2/test/compiler/style_compiler_import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// used by style_compiler_spec.ts
export var STYLES = ['span {color: blue}'];
Loading

0 comments on commit 2384082

Please sign in to comment.