-
Notifications
You must be signed in to change notification settings - Fork 25.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(compiler): add stylesheet compiler
- Loading branch information
Showing
13 changed files
with
486 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}')`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}']; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}']; |
Oops, something went wrong.