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 { 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 { 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 { 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 implements TemplateAstVisitor { result: R[] = []; transitiveNgContentCount: number = 0; constructor(public commandFactory: CommandFactory, 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(',')}]`; }