
BREAKING CHANGE All imports from 'angular2/core/compiler' should be changed to 'angular2/compiler'.
394 lines
18 KiB
TypeScript
394 lines
18 KiB
TypeScript
import {isPresent, isBlank, Type, isString, StringWrapper} from 'angular2/src/core/facade/lang';
|
|
import {SetWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
|
import {
|
|
TemplateCmd,
|
|
text,
|
|
ngContent,
|
|
beginElement,
|
|
endElement,
|
|
beginComponent,
|
|
endComponent,
|
|
embeddedTemplate,
|
|
CompiledTemplate
|
|
} from 'angular2/src/core/linker/template_commands';
|
|
import {
|
|
TemplateAst,
|
|
TemplateAstVisitor,
|
|
NgContentAst,
|
|
EmbeddedTemplateAst,
|
|
ElementAst,
|
|
VariableAst,
|
|
BoundEventAst,
|
|
BoundElementPropertyAst,
|
|
AttrAst,
|
|
BoundTextAst,
|
|
TextAst,
|
|
DirectiveAst,
|
|
BoundDirectivePropertyAst,
|
|
templateVisitAll
|
|
} from './template_ast';
|
|
import {CompileTypeMetadata, CompileDirectiveMetadata} from './directive_metadata';
|
|
import {SourceExpressions, SourceExpression, moduleRef} from './source_module';
|
|
|
|
import {ViewEncapsulation} from 'angular2/src/core/metadata/view';
|
|
import {
|
|
shimHostAttribute,
|
|
shimContentAttribute,
|
|
shimContentAttributeExpr,
|
|
shimHostAttributeExpr
|
|
} from './style_compiler';
|
|
import {escapeSingleQuoteString, MODULE_SUFFIX} from './util';
|
|
import {Injectable} from 'angular2/src/core/di';
|
|
|
|
export var TEMPLATE_COMMANDS_MODULE_REF =
|
|
moduleRef(`package:angular2/src/core/linker/template_commands${MODULE_SUFFIX}`);
|
|
|
|
const IMPLICIT_TEMPLATE_VAR = '\$implicit';
|
|
const CLASS_ATTR = 'class';
|
|
const STYLE_ATTR = 'style';
|
|
|
|
@Injectable()
|
|
export class CommandCompiler {
|
|
compileComponentRuntime(component: CompileDirectiveMetadata, appId: string, templateId: number,
|
|
template: TemplateAst[], changeDetectorFactories: Function[],
|
|
componentTemplateFactory: Function): TemplateCmd[] {
|
|
var visitor = new CommandBuilderVisitor(
|
|
new RuntimeCommandFactory(component, appId, templateId, componentTemplateFactory,
|
|
changeDetectorFactories),
|
|
0);
|
|
templateVisitAll(visitor, template);
|
|
return visitor.result;
|
|
}
|
|
|
|
compileComponentCodeGen(component: CompileDirectiveMetadata, appIdExpr: string,
|
|
templateIdExpr: string, template: TemplateAst[],
|
|
changeDetectorFactoryExpressions: string[],
|
|
componentTemplateFactory: Function): SourceExpression {
|
|
var visitor = new CommandBuilderVisitor(
|
|
new CodegenCommandFactory(component, appIdExpr, templateIdExpr, componentTemplateFactory,
|
|
changeDetectorFactoryExpressions),
|
|
0);
|
|
templateVisitAll(visitor, template);
|
|
var source = `[${visitor.result.join(',')}]`;
|
|
return new SourceExpression([], source);
|
|
}
|
|
}
|
|
|
|
interface CommandFactory<R> {
|
|
createText(value: string, isBound: boolean, ngContentIndex: number): R;
|
|
createNgContent(index: number, ngContentIndex: number): R;
|
|
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
isBound: boolean, ngContentIndex: number): R;
|
|
createEndElement(): R;
|
|
createBeginComponent(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
nativeShadow: boolean, ngContentIndex: number): R;
|
|
createEndComponent(): R;
|
|
createEmbeddedTemplate(embeddedTemplateIndex: number, attrNameAndValues: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
isMerged: boolean, ngContentIndex: number, children: R[]): R;
|
|
}
|
|
|
|
class RuntimeCommandFactory implements CommandFactory<TemplateCmd> {
|
|
constructor(private component: CompileDirectiveMetadata, private appId: string,
|
|
private templateId: number, private componentTemplateFactory: Function,
|
|
private changeDetectorFactories: Function[]) {}
|
|
private _mapDirectives(directives: CompileDirectiveMetadata[]): Type[] {
|
|
return directives.map(directive => directive.type.runtime);
|
|
}
|
|
private _addStyleShimAttributes(attrNameAndValues: string[],
|
|
localComponent: CompileDirectiveMetadata,
|
|
localTemplateId: number): string[] {
|
|
var additionalStyles = [];
|
|
if (isPresent(localComponent) &&
|
|
localComponent.template.encapsulation === ViewEncapsulation.Emulated) {
|
|
additionalStyles.push(shimHostAttribute(this.appId, localTemplateId));
|
|
additionalStyles.push('');
|
|
}
|
|
if (this.component.template.encapsulation === ViewEncapsulation.Emulated) {
|
|
additionalStyles.push(shimContentAttribute(this.appId, this.templateId));
|
|
additionalStyles.push('');
|
|
}
|
|
return additionalStyles.concat(attrNameAndValues);
|
|
}
|
|
|
|
createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd {
|
|
return text(value, isBound, ngContentIndex);
|
|
}
|
|
createNgContent(index: number, ngContentIndex: number): TemplateCmd {
|
|
return ngContent(index, ngContentIndex);
|
|
}
|
|
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
isBound: boolean, ngContentIndex: number): TemplateCmd {
|
|
return beginElement(name, this._addStyleShimAttributes(attrNameAndValues, null, null),
|
|
eventTargetAndNames, variableNameAndValues, this._mapDirectives(directives),
|
|
isBound, ngContentIndex);
|
|
}
|
|
createEndElement(): TemplateCmd { return endElement(); }
|
|
createBeginComponent(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
nativeShadow: boolean, ngContentIndex: number): TemplateCmd {
|
|
var nestedTemplate = this.componentTemplateFactory(directives[0]);
|
|
return beginComponent(
|
|
name, this._addStyleShimAttributes(attrNameAndValues, directives[0], nestedTemplate.id),
|
|
eventTargetAndNames, variableNameAndValues, this._mapDirectives(directives), nativeShadow,
|
|
ngContentIndex, nestedTemplate);
|
|
}
|
|
createEndComponent(): TemplateCmd { return endComponent(); }
|
|
createEmbeddedTemplate(embeddedTemplateIndex: number, attrNameAndValues: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
isMerged: boolean, ngContentIndex: number,
|
|
children: TemplateCmd[]): TemplateCmd {
|
|
return embeddedTemplate(attrNameAndValues, variableNameAndValues,
|
|
this._mapDirectives(directives), isMerged, ngContentIndex,
|
|
this.changeDetectorFactories[embeddedTemplateIndex], children);
|
|
}
|
|
}
|
|
|
|
class CodegenCommandFactory implements CommandFactory<string> {
|
|
constructor(private component: CompileDirectiveMetadata, private appIdExpr: string,
|
|
private templateIdExpr: string, private componentTemplateFactory: Function,
|
|
private changeDetectorFactoryExpressions: string[]) {}
|
|
|
|
private _addStyleShimAttributes(attrNameAndValues: string[],
|
|
localComponent: CompileDirectiveMetadata,
|
|
localTemplateIdExpr: string): any[] {
|
|
var additionalStlyes = [];
|
|
if (isPresent(localComponent) &&
|
|
localComponent.template.encapsulation === ViewEncapsulation.Emulated) {
|
|
additionalStlyes.push(
|
|
new Expression(shimHostAttributeExpr(this.appIdExpr, localTemplateIdExpr)));
|
|
additionalStlyes.push('');
|
|
}
|
|
if (this.component.template.encapsulation === ViewEncapsulation.Emulated) {
|
|
additionalStlyes.push(
|
|
new Expression(shimContentAttributeExpr(this.appIdExpr, this.templateIdExpr)));
|
|
additionalStlyes.push('');
|
|
}
|
|
return additionalStlyes.concat(attrNameAndValues);
|
|
}
|
|
|
|
createText(value: string, isBound: boolean, ngContentIndex: number): string {
|
|
return `${TEMPLATE_COMMANDS_MODULE_REF}text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`;
|
|
}
|
|
createNgContent(index: number, ngContentIndex: number): string {
|
|
return `${TEMPLATE_COMMANDS_MODULE_REF}ngContent(${index}, ${ngContentIndex})`;
|
|
}
|
|
createBeginElement(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
isBound: boolean, ngContentIndex: number): string {
|
|
var attrsExpression = codeGenArray(this._addStyleShimAttributes(attrNameAndValues, null, null));
|
|
return `${TEMPLATE_COMMANDS_MODULE_REF}beginElement(${escapeSingleQuoteString(name)}, ${attrsExpression}, ${codeGenArray(eventTargetAndNames)}, ${codeGenArray(variableNameAndValues)}, ${codeGenDirectivesArray(directives)}, ${isBound}, ${ngContentIndex})`;
|
|
}
|
|
createEndElement(): string { return `${TEMPLATE_COMMANDS_MODULE_REF}endElement()`; }
|
|
createBeginComponent(name: string, attrNameAndValues: string[], eventTargetAndNames: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
nativeShadow: boolean, ngContentIndex: number): string {
|
|
var nestedCompExpr = this.componentTemplateFactory(directives[0]);
|
|
var attrsExpression = codeGenArray(
|
|
this._addStyleShimAttributes(attrNameAndValues, directives[0], `${nestedCompExpr}.id`));
|
|
return `${TEMPLATE_COMMANDS_MODULE_REF}beginComponent(${escapeSingleQuoteString(name)}, ${attrsExpression}, ${codeGenArray(eventTargetAndNames)}, ${codeGenArray(variableNameAndValues)}, ${codeGenDirectivesArray(directives)}, ${nativeShadow}, ${ngContentIndex}, ${nestedCompExpr})`;
|
|
}
|
|
createEndComponent(): string { return `${TEMPLATE_COMMANDS_MODULE_REF}endComponent()`; }
|
|
createEmbeddedTemplate(embeddedTemplateIndex: number, attrNameAndValues: string[],
|
|
variableNameAndValues: string[], directives: CompileDirectiveMetadata[],
|
|
isMerged: boolean, ngContentIndex: number, children: string[]): string {
|
|
return `${TEMPLATE_COMMANDS_MODULE_REF}embeddedTemplate(${codeGenArray(attrNameAndValues)}, ${codeGenArray(variableNameAndValues)}, ` +
|
|
`${codeGenDirectivesArray(directives)}, ${isMerged}, ${ngContentIndex}, ${this.changeDetectorFactoryExpressions[embeddedTemplateIndex]}, [${children.join(',')}])`;
|
|
}
|
|
}
|
|
|
|
function visitAndReturnContext(visitor: TemplateAstVisitor, asts: TemplateAst[],
|
|
context: any): any {
|
|
templateVisitAll(visitor, asts, context);
|
|
return context;
|
|
}
|
|
|
|
class CommandBuilderVisitor<R> implements TemplateAstVisitor {
|
|
result: R[] = [];
|
|
transitiveNgContentCount: number = 0;
|
|
constructor(public commandFactory: CommandFactory<R>, public embeddedTemplateIndex: number) {}
|
|
|
|
private _readAttrNameAndValues(directives: CompileDirectiveMetadata[],
|
|
attrAsts: TemplateAst[]): string[] {
|
|
var attrs = keyValueArrayToMap(visitAndReturnContext(this, attrAsts, []));
|
|
directives.forEach(directiveMeta => {
|
|
StringMapWrapper.forEach(directiveMeta.hostAttributes, (value, name) => {
|
|
var prevValue = attrs[name];
|
|
attrs[name] = isPresent(prevValue) ? mergeAttributeValue(name, prevValue, value) : value;
|
|
});
|
|
});
|
|
return mapToKeyValueArray(attrs);
|
|
}
|
|
|
|
visitNgContent(ast: NgContentAst, context: any): any {
|
|
this.transitiveNgContentCount++;
|
|
this.result.push(this.commandFactory.createNgContent(ast.index, ast.ngContentIndex));
|
|
return null;
|
|
}
|
|
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
|
|
this.embeddedTemplateIndex++;
|
|
var childVisitor = new CommandBuilderVisitor(this.commandFactory, this.embeddedTemplateIndex);
|
|
templateVisitAll(childVisitor, ast.children);
|
|
var isMerged = childVisitor.transitiveNgContentCount > 0;
|
|
var variableNameAndValues = [];
|
|
ast.vars.forEach((varAst) => {
|
|
variableNameAndValues.push(varAst.name);
|
|
variableNameAndValues.push(varAst.value.length > 0 ? varAst.value : IMPLICIT_TEMPLATE_VAR);
|
|
});
|
|
var directives = [];
|
|
ListWrapper.forEachWithIndex(ast.directives, (directiveAst: DirectiveAst, index: number) => {
|
|
directiveAst.visit(this, new DirectiveContext(index, [], [], directives));
|
|
});
|
|
this.result.push(this.commandFactory.createEmbeddedTemplate(
|
|
this.embeddedTemplateIndex, this._readAttrNameAndValues(directives, ast.attrs),
|
|
variableNameAndValues, directives, isMerged, ast.ngContentIndex, childVisitor.result));
|
|
this.transitiveNgContentCount += childVisitor.transitiveNgContentCount;
|
|
this.embeddedTemplateIndex = childVisitor.embeddedTemplateIndex;
|
|
return null;
|
|
}
|
|
visitElement(ast: ElementAst, context: any): any {
|
|
var component = ast.getComponent();
|
|
var eventTargetAndNames = visitAndReturnContext(this, ast.outputs, []);
|
|
var variableNameAndValues = [];
|
|
if (isBlank(component)) {
|
|
ast.exportAsVars.forEach((varAst) => {
|
|
variableNameAndValues.push(varAst.name);
|
|
variableNameAndValues.push(null);
|
|
});
|
|
}
|
|
var directives = [];
|
|
ListWrapper.forEachWithIndex(ast.directives, (directiveAst: DirectiveAst, index: number) => {
|
|
directiveAst.visit(this, new DirectiveContext(index, eventTargetAndNames,
|
|
variableNameAndValues, directives));
|
|
});
|
|
eventTargetAndNames = removeKeyValueArrayDuplicates(eventTargetAndNames);
|
|
|
|
var attrNameAndValues = this._readAttrNameAndValues(directives, ast.attrs);
|
|
if (isPresent(component)) {
|
|
this.result.push(this.commandFactory.createBeginComponent(
|
|
ast.name, attrNameAndValues, eventTargetAndNames, variableNameAndValues, directives,
|
|
component.template.encapsulation === ViewEncapsulation.Native, ast.ngContentIndex));
|
|
templateVisitAll(this, ast.children);
|
|
this.result.push(this.commandFactory.createEndComponent());
|
|
} else {
|
|
this.result.push(this.commandFactory.createBeginElement(
|
|
ast.name, attrNameAndValues, eventTargetAndNames, variableNameAndValues, directives,
|
|
ast.isBound(), ast.ngContentIndex));
|
|
templateVisitAll(this, ast.children);
|
|
this.result.push(this.commandFactory.createEndElement());
|
|
}
|
|
return null;
|
|
}
|
|
visitVariable(ast: VariableAst, ctx: any): any { return null; }
|
|
visitAttr(ast: AttrAst, attrNameAndValues: string[]): any {
|
|
attrNameAndValues.push(ast.name);
|
|
attrNameAndValues.push(ast.value);
|
|
return null;
|
|
}
|
|
visitBoundText(ast: BoundTextAst, context: any): any {
|
|
this.result.push(this.commandFactory.createText(null, true, ast.ngContentIndex));
|
|
return null;
|
|
}
|
|
visitText(ast: TextAst, context: any): any {
|
|
this.result.push(this.commandFactory.createText(ast.value, false, ast.ngContentIndex));
|
|
return null;
|
|
}
|
|
visitDirective(ast: DirectiveAst, ctx: DirectiveContext): any {
|
|
ctx.targetDirectives.push(ast.directive);
|
|
templateVisitAll(this, ast.hostEvents, ctx.eventTargetAndNames);
|
|
ast.exportAsVars.forEach(varAst => {
|
|
ctx.targetVariableNameAndValues.push(varAst.name);
|
|
ctx.targetVariableNameAndValues.push(ctx.index);
|
|
});
|
|
return null;
|
|
}
|
|
visitEvent(ast: BoundEventAst, eventTargetAndNames: string[]): any {
|
|
eventTargetAndNames.push(ast.target);
|
|
eventTargetAndNames.push(ast.name);
|
|
return null;
|
|
}
|
|
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; }
|
|
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
|
|
}
|
|
|
|
function removeKeyValueArrayDuplicates(keyValueArray: string[]): string[] {
|
|
var knownPairs = new Set();
|
|
var resultKeyValueArray = [];
|
|
for (var i = 0; i < keyValueArray.length; i += 2) {
|
|
var key = keyValueArray[i];
|
|
var value = keyValueArray[i + 1];
|
|
var pairId = `${key}:${value}`;
|
|
if (!SetWrapper.has(knownPairs, pairId)) {
|
|
resultKeyValueArray.push(key);
|
|
resultKeyValueArray.push(value);
|
|
knownPairs.add(pairId);
|
|
}
|
|
}
|
|
return resultKeyValueArray;
|
|
}
|
|
|
|
function keyValueArrayToMap(keyValueArr: string[]): {[key: string]: string} {
|
|
var data: {[key: string]: string} = {};
|
|
for (var i = 0; i < keyValueArr.length; i += 2) {
|
|
data[keyValueArr[i]] = keyValueArr[i + 1];
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function mapToKeyValueArray(data: {[key: string]: string}): string[] {
|
|
var entryArray = [];
|
|
StringMapWrapper.forEach(data, (value, name) => { entryArray.push([name, value]); });
|
|
// We need to sort to get a defined output order
|
|
// for tests and for caching generated artifacts...
|
|
ListWrapper.sort(entryArray, (entry1, entry2) => StringWrapper.compare(entry1[0], entry2[0]));
|
|
var keyValueArray = [];
|
|
entryArray.forEach((entry) => {
|
|
keyValueArray.push(entry[0]);
|
|
keyValueArray.push(entry[1]);
|
|
});
|
|
return keyValueArray;
|
|
}
|
|
|
|
function mergeAttributeValue(attrName: string, attrValue1: string, attrValue2: string): string {
|
|
if (attrName == CLASS_ATTR || attrName == STYLE_ATTR) {
|
|
return `${attrValue1} ${attrValue2}`;
|
|
} else {
|
|
return attrValue2;
|
|
}
|
|
}
|
|
|
|
class DirectiveContext {
|
|
constructor(public index: number, public eventTargetAndNames: string[],
|
|
public targetVariableNameAndValues: any[],
|
|
public targetDirectives: CompileDirectiveMetadata[]) {}
|
|
}
|
|
|
|
class Expression {
|
|
constructor(public value: string) {}
|
|
}
|
|
|
|
function escapeValue(value: any): string {
|
|
if (value instanceof Expression) {
|
|
return value.value;
|
|
} else if (isString(value)) {
|
|
return escapeSingleQuoteString(value);
|
|
} else if (isBlank(value)) {
|
|
return 'null';
|
|
} else {
|
|
return `${value}`;
|
|
}
|
|
}
|
|
|
|
function codeGenArray(data: any[]): string {
|
|
return `[${data.map(escapeValue).join(',')}]`;
|
|
}
|
|
|
|
function codeGenDirectivesArray(directives: CompileDirectiveMetadata[]): string {
|
|
var expressions = directives.map(
|
|
directiveType => `${moduleRef(directiveType.type.moduleUrl)}${directiveType.type.name}`);
|
|
return `[${expressions.join(',')}]`;
|
|
}
|