feat(compiler): support creating template commands

Closes #4142
This commit is contained in:
Tobias Bosch
2015-09-11 13:37:05 -07:00
parent 71cbb49672
commit 0246b2a2cb
13 changed files with 1324 additions and 166 deletions

View File

@ -134,7 +134,7 @@ export class TemplateMetadata {
this.ngContentSelectors = ngContentSelectors;
}
static fromJson(data: StringMap<string, any>):TemplateMetadata {
static fromJson(data: StringMap<string, any>): TemplateMetadata {
return new TemplateMetadata({
encapsulation: isPresent(data['encapsulation']) ?
viewEncapsulationFromJson(data['encapsulation']) :

View File

@ -36,47 +36,43 @@ import {
export function createChangeDetectorDefinitions(
componentType: TypeMetadata, componentStrategy: ChangeDetectionStrategy,
genConfig: ChangeDetectorGenConfig, parsedTemplate: TemplateAst[]): ChangeDetectorDefinition[] {
var visitor = new ProtoViewVisitor(componentStrategy);
var pvVisitors = [];
var visitor = new ProtoViewVisitor(null, pvVisitors, componentStrategy);
templateVisitAll(visitor, parsedTemplate);
return createChangeDefinitions(visitor.allProtoViews, componentType, genConfig);
return createChangeDefinitions(pvVisitors, componentType, genConfig);
}
class ProtoViewVisitor implements TemplateAstVisitor {
viewCount: number = 0;
protoViewStack: ProtoViewVisitorData[] = [];
allProtoViews: ProtoViewVisitorData[] = [];
viewIndex: number;
boundTextCount: number = 0;
boundElementCount: number = 0;
variableNames: string[] = [];
bindingRecords: BindingRecord[] = [];
eventRecords: BindingRecord[] = [];
directiveRecords: DirectiveRecord[] = [];
constructor(componentStrategy: ChangeDetectionStrategy) {
this._beginProtoView(new ProtoViewVisitorData(null, componentStrategy, this.viewCount++));
}
private _beginProtoView(data: ProtoViewVisitorData) {
this.protoViewStack.push(data);
this.allProtoViews.push(data);
}
get currentProtoView(): ProtoViewVisitorData {
return this.protoViewStack[this.protoViewStack.length - 1];
constructor(public parent: ProtoViewVisitor, public allVisitors: ProtoViewVisitor[],
public strategy: ChangeDetectionStrategy) {
this.viewIndex = allVisitors.length;
allVisitors.push(this);
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
this.currentProtoView.boundElementCount++;
this.boundElementCount++;
templateVisitAll(this, ast.directives);
this.viewCount++;
this._beginProtoView(new ProtoViewVisitorData(
this.currentProtoView, ChangeDetectionStrategy.Default, this.viewCount - 1));
var childVisitor =
new ProtoViewVisitor(this, this.allVisitors, ChangeDetectionStrategy.Default);
// Attention: variables present on an embedded template count towards
// the embedded template and not the template anchor!
templateVisitAll(this, ast.vars);
templateVisitAll(this, ast.children);
this.protoViewStack.pop();
templateVisitAll(childVisitor, ast.vars);
templateVisitAll(childVisitor, ast.children);
return null;
}
visitElement(ast: ElementAst, context: any): any {
if (ast.isBound()) {
this.currentProtoView.boundElementCount++;
this.boundElementCount++;
}
templateVisitAll(this, ast.properties, null);
templateVisitAll(this, ast.events);
@ -91,7 +87,7 @@ class ProtoViewVisitor implements TemplateAstVisitor {
visitNgContent(ast: NgContentAst, context: any): any { return null; }
visitVariable(ast: VariableAst, context: any): any {
this.currentProtoView.variableNames.push(ast.name);
this.variableNames.push(ast.name);
return null;
}
@ -99,14 +95,13 @@ class ProtoViewVisitor implements TemplateAstVisitor {
var bindingRecord =
isPresent(directiveRecord) ?
BindingRecord.createForHostEvent(ast.handler, ast.name, directiveRecord) :
BindingRecord.createForEvent(ast.handler, ast.name,
this.currentProtoView.boundElementCount - 1);
this.currentProtoView.eventRecords.push(bindingRecord);
BindingRecord.createForEvent(ast.handler, ast.name, this.boundElementCount - 1);
this.eventRecords.push(bindingRecord);
return null;
}
visitElementProperty(ast: BoundElementPropertyAst, directiveRecord: DirectiveRecord): any {
var boundElementIndex = this.currentProtoView.boundElementCount - 1;
var boundElementIndex = this.boundElementCount - 1;
var dirIndex = isPresent(directiveRecord) ? directiveRecord.directiveIndex : null;
var bindingRecord;
if (ast.type === PropertyBindingType.Property) {
@ -130,20 +125,18 @@ class ProtoViewVisitor implements TemplateAstVisitor {
BindingRecord.createForHostStyle(dirIndex, ast.value, ast.name, ast.unit) :
BindingRecord.createForElementStyle(ast.value, boundElementIndex, ast.name, ast.unit);
}
this.currentProtoView.bindingRecords.push(bindingRecord);
this.bindingRecords.push(bindingRecord);
return null;
}
visitAttr(ast: AttrAst, context: any): any { return null; }
visitBoundText(ast: BoundTextAst, context: any): any {
var boundTextIndex = this.currentProtoView.boundTextCount++;
this.currentProtoView.bindingRecords.push(
BindingRecord.createForTextNode(ast.value, boundTextIndex));
var boundTextIndex = this.boundTextCount++;
this.bindingRecords.push(BindingRecord.createForTextNode(ast.value, boundTextIndex));
return null;
}
visitText(ast: TextAst, context: any): any { return null; }
visitDirective(ast: DirectiveAst, directiveIndexAsNumber: number): any {
var directiveIndex =
new DirectiveIndex(this.currentProtoView.boundElementCount - 1, directiveIndexAsNumber);
var directiveIndex = new DirectiveIndex(this.boundElementCount - 1, directiveIndexAsNumber);
var directiveMetadata = ast.directive;
var changeDetectionMeta = directiveMetadata.changeDetection;
var directiveRecord = new DirectiveRecord({
@ -157,10 +150,10 @@ class ProtoViewVisitor implements TemplateAstVisitor {
callOnInit: changeDetectionMeta.callOnInit,
changeDetection: changeDetectionMeta.changeDetection
});
this.currentProtoView.directiveRecords.push(directiveRecord);
this.directiveRecords.push(directiveRecord);
templateVisitAll(this, ast.properties, directiveRecord);
var bindingRecords = this.currentProtoView.bindingRecords;
var bindingRecords = this.bindingRecords;
if (directiveRecord.callOnChanges) {
bindingRecords.push(BindingRecord.createDirectiveOnChanges(directiveRecord));
}
@ -178,39 +171,29 @@ class ProtoViewVisitor implements TemplateAstVisitor {
// TODO: these setters should eventually be created by change detection, to make
// it monomorphic!
var setter = reflector.setter(ast.directiveName);
this.currentProtoView.bindingRecords.push(
this.bindingRecords.push(
BindingRecord.createForDirective(ast.value, ast.directiveName, setter, directiveRecord));
return null;
}
}
class ProtoViewVisitorData {
boundTextCount: number = 0;
boundElementCount: number = 0;
variableNames: string[] = [];
bindingRecords: BindingRecord[] = [];
eventRecords: BindingRecord[] = [];
directiveRecords: DirectiveRecord[] = [];
constructor(public parent: ProtoViewVisitorData, public strategy: ChangeDetectionStrategy,
public viewIndex: number) {}
}
function createChangeDefinitions(pvDatas: ProtoViewVisitorData[], componentType: TypeMetadata,
function createChangeDefinitions(pvVisitors: ProtoViewVisitor[], componentType: TypeMetadata,
genConfig: ChangeDetectorGenConfig): ChangeDetectorDefinition[] {
var pvVariableNames = _collectNestedProtoViewsVariableNames(pvDatas);
return pvDatas.map(pvData => {
var viewType = pvData.viewIndex === 0 ? 'component' : 'embedded';
var id = _protoViewId(componentType, pvData.viewIndex, viewType);
return new ChangeDetectorDefinition(id, pvData.strategy, pvVariableNames[pvData.viewIndex],
pvData.bindingRecords, pvData.eventRecords,
pvData.directiveRecords, genConfig);
var pvVariableNames = _collectNestedProtoViewsVariableNames(pvVisitors);
return pvVisitors.map(pvVisitor => {
var viewType = pvVisitor.viewIndex === 0 ? 'component' : 'embedded';
var id = _protoViewId(componentType, pvVisitor.viewIndex, viewType);
return new ChangeDetectorDefinition(
id, pvVisitor.strategy, pvVariableNames[pvVisitor.viewIndex], pvVisitor.bindingRecords,
pvVisitor.eventRecords, pvVisitor.directiveRecords, genConfig);
});
}
function _collectNestedProtoViewsVariableNames(pvs: ProtoViewVisitorData[]): string[][] {
var nestedPvVariableNames: string[][] = ListWrapper.createFixedSize(pvs.length);
pvs.forEach((pv) => {
function _collectNestedProtoViewsVariableNames(pvVisitors: ProtoViewVisitor[]): string[][] {
var nestedPvVariableNames: string[][] = ListWrapper.createFixedSize(pvVisitors.length);
pvVisitors.forEach((pv) => {
var parentVariableNames: string[] =
isPresent(pv.parent) ? nestedPvVariableNames[pv.parent.viewIndex] : [];
nestedPvVariableNames[pv.viewIndex] = parentVariableNames.concat(pv.variableNames);

View File

@ -0,0 +1,241 @@
import {isPresent, Type} from 'angular2/src/core/facade/lang';
import {
TemplateCmd,
text,
ngContent,
beginElement,
endElement,
beginComponent,
endComponent,
embeddedTemplate
} from 'angular2/src/core/compiler/template_commands';
import {
TemplateAst,
TemplateAstVisitor,
NgContentAst,
EmbeddedTemplateAst,
ElementAst,
VariableAst,
BoundEventAst,
BoundElementPropertyAst,
AttrAst,
BoundTextAst,
TextAst,
DirectiveAst,
BoundDirectivePropertyAst,
templateVisitAll
} from './template_ast';
import {SourceModule, DirectiveMetadata, TypeMetadata} from './api';
import {ViewEncapsulation} from 'angular2/src/core/render/api';
import {shimHostAttribute, shimContentAttribute} from './style_compiler';
import {escapeSingleQuoteString} from './util';
const TEMPLATE_COMMANDS_MODULE = 'angular2/src/core/compiler/template_commands';
const TEMPLATE_COMMANDS_MODULE_ALIAS = 'tc';
export class CommandCompiler {
compileComponentRuntime(component: DirectiveMetadata, template: TemplateAst[],
componentTemplateFactory: Function): TemplateCmd[] {
var visitor =
new CommandBuilderVisitor(new RuntimeCommandFactory(componentTemplateFactory), component);
templateVisitAll(visitor, template);
return visitor.result;
}
compileComponentCodeGen(component: DirectiveMetadata, template: TemplateAst[],
componentTemplateFactory: Function): SourceModule {
var imports: string[][] = [[TEMPLATE_COMMANDS_MODULE, TEMPLATE_COMMANDS_MODULE_ALIAS]];
var visitor = new CommandBuilderVisitor(
new CodegenCommandFactory(componentTemplateFactory, TEMPLATE_COMMANDS_MODULE_ALIAS,
imports),
component);
templateVisitAll(visitor, template);
var source = `var COMMANDS = [${visitor.result.join(',')}];`;
return new SourceModule(null, source, imports);
}
}
interface CommandFactory<R> {
createText(value: string, isBound: boolean, ngContentIndex: number): R;
createNgContent(ngContentIndex: number): R;
createBeginElement(name: string, attrNameAndValues: string[], eventNames: string[],
variableNameAndValues: string[], directives: TypeMetadata[], isBound: boolean,
ngContentIndex: number): R;
createEndElement(): R;
createBeginComponent(name: string, attrNameAndValues: string[], eventNames: string[],
variableNameAndValues: string[], directives: TypeMetadata[],
nativeShadow: boolean, ngContentIndex: number): R;
createEndComponent(): R;
createEmbeddedTemplate(attrNameAndValues: string[], variableNameAndValues: string[],
directives: TypeMetadata[], isMerged: boolean, ngContentIndex: number,
children: R[]): R;
}
class RuntimeCommandFactory implements CommandFactory<TemplateCmd> {
constructor(public componentTemplateFactory: Function) {}
private _mapDirectives(directives: TypeMetadata[]): Type[] {
return directives.map(directive => directive.type);
}
createText(value: string, isBound: boolean, ngContentIndex: number): TemplateCmd {
return text(value, isBound, ngContentIndex);
}
createNgContent(ngContentIndex: number): TemplateCmd { return ngContent(ngContentIndex); }
createBeginElement(name: string, attrNameAndValues: string[], eventNames: string[],
variableNameAndValues: string[], directives: TypeMetadata[], isBound: boolean,
ngContentIndex: number): TemplateCmd {
return beginElement(name, attrNameAndValues, eventNames, variableNameAndValues,
this._mapDirectives(directives), isBound, ngContentIndex);
}
createEndElement(): TemplateCmd { return endElement(); }
createBeginComponent(name: string, attrNameAndValues: string[], eventNames: string[],
variableNameAndValues: string[], directives: TypeMetadata[],
nativeShadow: boolean, ngContentIndex: number): TemplateCmd {
return beginComponent(name, attrNameAndValues, eventNames, variableNameAndValues,
this._mapDirectives(directives), nativeShadow, ngContentIndex,
this.componentTemplateFactory(directives[0]));
}
createEndComponent(): TemplateCmd { return endComponent(); }
createEmbeddedTemplate(attrNameAndValues: string[], variableNameAndValues: string[],
directives: TypeMetadata[], isMerged: boolean, ngContentIndex: number,
children: TemplateCmd[]): TemplateCmd {
return embeddedTemplate(attrNameAndValues, variableNameAndValues,
this._mapDirectives(directives), isMerged, ngContentIndex, children);
}
}
function escapeStringArray(data: string[]): string {
return `[${data.map( value => escapeSingleQuoteString(value)).join(',')}]`;
}
class CodegenCommandFactory implements CommandFactory<string> {
constructor(public componentTemplateFactory: Function, public templateCommandsModuleAlias,
public imports: string[][]) {}
private _escapeDirectives(directives: TypeMetadata[]): string[] {
return directives.map(directiveType => {
var importAlias = `dir${this.imports.length}`;
this.imports.push([directiveType.typeUrl, importAlias]);
return `${importAlias}.${directiveType.typeName}`;
});
}
createText(value: string, isBound: boolean, ngContentIndex: number): string {
return `${this.templateCommandsModuleAlias}.text(${escapeSingleQuoteString(value)}, ${isBound}, ${ngContentIndex})`;
}
createNgContent(ngContentIndex: number): string {
return `${this.templateCommandsModuleAlias}.ngContent(${ngContentIndex})`;
}
createBeginElement(name: string, attrNameAndValues: string[], eventNames: string[],
variableNameAndValues: string[], directives: TypeMetadata[], isBound: boolean,
ngContentIndex: number): string {
return `${this.templateCommandsModuleAlias}.beginElement(${escapeSingleQuoteString(name)}, ${escapeStringArray(attrNameAndValues)}, ${escapeStringArray(eventNames)}, ${escapeStringArray(variableNameAndValues)}, [${this._escapeDirectives(directives).join(',')}], ${isBound}, ${ngContentIndex})`;
}
createEndElement(): string { return `${this.templateCommandsModuleAlias}.endElement()`; }
createBeginComponent(name: string, attrNameAndValues: string[], eventNames: string[],
variableNameAndValues: string[], directives: TypeMetadata[],
nativeShadow: boolean, ngContentIndex: number): string {
return `${this.templateCommandsModuleAlias}.beginComponent(${escapeSingleQuoteString(name)}, ${escapeStringArray(attrNameAndValues)}, ${escapeStringArray(eventNames)}, ${escapeStringArray(variableNameAndValues)}, [${this._escapeDirectives(directives).join(',')}], ${nativeShadow}, ${ngContentIndex}, ${this.componentTemplateFactory(directives[0], this.imports)})`;
}
createEndComponent(): string { return `${this.templateCommandsModuleAlias}.endComponent()`; }
createEmbeddedTemplate(attrNameAndValues: string[], variableNameAndValues: string[],
directives: TypeMetadata[], isMerged: boolean, ngContentIndex: number,
children: string[]): string {
return `${this.templateCommandsModuleAlias}.embeddedTemplate(${escapeStringArray(attrNameAndValues)}, ${escapeStringArray(variableNameAndValues)}, [${this._escapeDirectives(directives).join(',')}], ${isMerged}, ${ngContentIndex}, [${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 component: DirectiveMetadata) {}
private _readAttrNameAndValues(localComponent: DirectiveMetadata,
attrAsts: TemplateAst[]): string[] {
var attrNameAndValues: string[] = visitAndReturnContext(this, attrAsts, []);
if (isPresent(localComponent) &&
localComponent.template.encapsulation === ViewEncapsulation.Emulated) {
attrNameAndValues.push(shimHostAttribute(localComponent.type));
attrNameAndValues.push('');
}
if (this.component.template.encapsulation === ViewEncapsulation.Emulated) {
attrNameAndValues.push(shimContentAttribute(this.component.type));
attrNameAndValues.push('');
}
return attrNameAndValues;
}
visitNgContent(ast: NgContentAst, context: any): any {
this.transitiveNgContentCount++;
this.result.push(this.commandFactory.createNgContent(ast.ngContentIndex));
return null;
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
var childVisitor = new CommandBuilderVisitor(this.commandFactory, this.component);
templateVisitAll(childVisitor, ast.children);
var isMerged = childVisitor.transitiveNgContentCount > 0;
this.transitiveNgContentCount += childVisitor.transitiveNgContentCount;
var directivesAndEventNames = visitAndReturnContext(this, ast.directives, [[], []]);
this.result.push(this.commandFactory.createEmbeddedTemplate(
this._readAttrNameAndValues(null, ast.attrs), visitAndReturnContext(this, ast.vars, []),
directivesAndEventNames[0], isMerged, ast.ngContentIndex, childVisitor.result));
return null;
}
visitElement(ast: ElementAst, context: any): any {
var component = ast.getComponent();
var eventNames = visitAndReturnContext(this, ast.events, []);
var directives = [];
visitAndReturnContext(this, ast.directives, [directives, eventNames]);
var attrNameAndValues = this._readAttrNameAndValues(component, ast.attrs);
var vars = visitAndReturnContext(this, ast.vars, []);
if (isPresent(component)) {
this.result.push(this.commandFactory.createBeginComponent(
ast.name, attrNameAndValues, eventNames, vars, 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,
eventNames, vars, directives,
ast.isBound(), ast.ngContentIndex));
templateVisitAll(this, ast.children);
this.result.push(this.commandFactory.createEndElement());
}
return null;
}
visitVariable(ast: VariableAst, variableNameAndValues: string[]): any {
variableNameAndValues.push(ast.name);
variableNameAndValues.push(ast.value);
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, directivesAndEventNames: any[][]): any {
directivesAndEventNames[0].push(ast.directive.type);
templateVisitAll(this, ast.hostEvents, directivesAndEventNames[1]);
return null;
}
visitEvent(ast: BoundEventAst, eventNames: string[]): any {
eventNames.push(ast.getFullName());
return null;
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; }
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
}

View File

@ -1,16 +1,17 @@
import {DirectiveMetadata, SourceModule, ViewEncapsulation} from './api';
import {DirectiveMetadata, SourceModule, TypeMetadata} from './api';
import {ViewEncapsulation} from 'angular2/src/core/render/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';
import {escapeSingleQuoteString} from './util';
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 {
@ -78,7 +79,7 @@ export class StyleCompiler {
var imports: string[][] = [];
var moduleSource = `var STYLES = (`;
moduleSource +=
`[${plainStyles.map( plainStyle => escapeString(this._shimIfNeeded(plainStyle, shim)) ).join(',')}]`;
`[${plainStyles.map( plainStyle => escapeSingleQuoteString(this._shimIfNeeded(plainStyle, shim)) ).join(',')}]`;
for (var i = 0; i < absUrls.length; i++) {
var url = absUrls[i];
var moduleAlias = `import${i}`;
@ -98,15 +99,12 @@ export class StyleCompiler {
}
}
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}'`;
export function shimContentAttribute(component: TypeMetadata): string {
return StringWrapper.replaceAll(CONTENT_ATTR, COMPONENT_REGEX, `${component.id}`);
}
export function shimHostAttribute(component: TypeMetadata): string {
return StringWrapper.replaceAll(HOST_ATTR, COMPONENT_REGEX, `${component.id}`);
}
function codeGenConcatArray(expression: string): string {

View File

@ -8,12 +8,12 @@ export interface TemplateAst {
}
export class TextAst implements TemplateAst {
constructor(public value: string, public sourceInfo: string) {}
constructor(public value: string, public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any { return visitor.visitText(this, context); }
}
export class BoundTextAst implements TemplateAst {
constructor(public value: AST, public sourceInfo: string) {}
constructor(public value: AST, public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitBoundText(this, context);
}
@ -38,6 +38,13 @@ export class BoundEventAst implements TemplateAst {
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEvent(this, context);
}
getFullName(): string {
if (isPresent(this.target)) {
return `${this.target}:${this.name}`;
} else {
return this.name;
}
}
}
export class VariableAst implements TemplateAst {
@ -48,9 +55,10 @@ export class VariableAst implements TemplateAst {
}
export class ElementAst implements TemplateAst {
constructor(public attrs: AttrAst[], public properties: BoundElementPropertyAst[],
public events: BoundEventAst[], public vars: VariableAst[],
public directives: DirectiveAst[], public children: TemplateAst[],
constructor(public name: string, public attrs: AttrAst[],
public properties: BoundElementPropertyAst[], public events: BoundEventAst[],
public vars: VariableAst[], public directives: DirectiveAst[],
public children: TemplateAst[], public ngContentIndex: number,
public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitElement(this, context);
@ -60,12 +68,18 @@ export class ElementAst implements TemplateAst {
return (this.properties.length > 0 || this.events.length > 0 || this.vars.length > 0 ||
this.directives.length > 0);
}
getComponent(): DirectiveMetadata {
return this.directives.length > 0 && this.directives[0].directive.isComponent ?
this.directives[0].directive :
null;
}
}
export class EmbeddedTemplateAst implements TemplateAst {
constructor(public attrs: AttrAst[], public vars: VariableAst[],
public directives: DirectiveAst[], public children: TemplateAst[],
public sourceInfo: string) {}
public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitEmbeddedTemplate(this, context);
}
@ -89,7 +103,7 @@ export class DirectiveAst implements TemplateAst {
}
export class NgContentAst implements TemplateAst {
constructor(public select: string, public sourceInfo: string) {}
constructor(public ngContentIndex: number, public sourceInfo: string) {}
visit(visitor: TemplateAstVisitor, context: any): any {
return visitor.visitNgContent(this, context);
}

View File

@ -12,7 +12,7 @@ import {BaseException} from 'angular2/src/core/facade/exceptions';
import {Parser, AST, ASTWithSource} from 'angular2/src/core/change_detection/change_detection';
import {TemplateBinding} from 'angular2/src/core/change_detection/parser/ast';
import {DirectiveMetadata} from './api';
import {DirectiveMetadata, TemplateMetadata} from './api';
import {
ElementAst,
BoundElementPropertyAst,
@ -54,7 +54,6 @@ import {dashCaseToCamelCase, camelCaseToDashCase} from './util';
var BIND_NAME_REGEXP =
/^(?:(?:(?:(bind-)|(var-|#)|(on-)|(bindon-))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/g;
const NG_CONTENT_SELECT_ATTR = 'select';
const NG_CONTENT_ELEMENT = 'ng-content';
const TEMPLATE_ELEMENT = 'template';
const TEMPLATE_ATTR = 'template';
@ -67,12 +66,14 @@ const ATTRIBUTE_PREFIX = 'attr';
const CLASS_PREFIX = 'class';
const STYLE_PREFIX = 'style';
var TEXT_CSS_SELECTOR = CssSelector.parse('*')[0];
export class TemplateParser {
constructor(private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry) {}
parse(domNodes: HtmlAst[], directives: DirectiveMetadata[]): TemplateAst[] {
var parseVisitor = new TemplateParseVisitor(directives, this._exprParser, this._schemaRegistry);
var result = htmlVisitAll(parseVisitor, domNodes);
var result = htmlVisitAll(parseVisitor, domNodes, EMPTY_COMPONENT);
if (parseVisitor.errors.length > 0) {
var errorString = parseVisitor.errors.join('\n');
throw new BaseException(`Template parse errors:\n${errorString}`);
@ -131,18 +132,21 @@ class TemplateParseVisitor implements HtmlAstVisitor {
}
}
visitText(ast: HtmlTextAst): any {
visitText(ast: HtmlTextAst, component: Component): any {
var ngContentIndex = component.findNgContentIndex(TEXT_CSS_SELECTOR);
var expr = this._parseInterpolation(ast.value, ast.sourceInfo);
if (isPresent(expr)) {
return new BoundTextAst(expr, ast.sourceInfo);
return new BoundTextAst(expr, ngContentIndex, ast.sourceInfo);
} else {
return new TextAst(ast.value, ast.sourceInfo);
return new TextAst(ast.value, ngContentIndex, ast.sourceInfo);
}
}
visitAttr(ast: HtmlAttrAst): any { return new AttrAst(ast.name, ast.value, ast.sourceInfo); }
visitAttr(ast: HtmlAttrAst, contex: any): any {
return new AttrAst(ast.name, ast.value, ast.sourceInfo);
}
visitElement(element: HtmlElementAst): any {
visitElement(element: HtmlElementAst, component: Component): any {
var nodeName = element.name;
var matchableAttrs: string[][] = [];
var elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = [];
@ -154,52 +158,53 @@ class TemplateParseVisitor implements HtmlAstVisitor {
var templateMatchableAttrs: string[][] = [];
var hasInlineTemplates = false;
var attrs = [];
var selectAttr = null;
element.attrs.forEach(attr => {
matchableAttrs.push([attr.name, attr.value]);
if (attr.name == NG_CONTENT_SELECT_ATTR) {
selectAttr = attr.value;
}
var hasBinding = this._parseAttr(attr, matchableAttrs, elementOrDirectiveProps, events, vars);
var hasTemplateBinding = this._parseInlineTemplateBinding(
attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateVars);
if (!hasBinding && !hasTemplateBinding) {
// don't include the bindings as attributes as well in the AST
attrs.push(this.visitAttr(attr));
attrs.push(this.visitAttr(attr, null));
}
if (hasTemplateBinding) {
hasInlineTemplates = true;
}
});
var elementCssSelector = this._createElementCssSelector(nodeName, matchableAttrs);
var directives = this._createDirectiveAsts(
element.name, this._parseDirectives(this.selectorMatcher, nodeName, matchableAttrs),
element.name, this._parseDirectives(this.selectorMatcher, elementCssSelector),
elementOrDirectiveProps, element.sourceInfo);
var elementProps: BoundElementPropertyAst[] =
this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directives);
var children = htmlVisitAll(this, element.children);
var children = htmlVisitAll(this, element.children, Component.create(directives));
var elementNgContentIndex =
hasInlineTemplates ? null : component.findNgContentIndex(elementCssSelector);
var parsedElement;
if (nodeName == NG_CONTENT_ELEMENT) {
parsedElement = new NgContentAst(selectAttr, element.sourceInfo);
parsedElement = new NgContentAst(elementNgContentIndex, element.sourceInfo);
} else if (nodeName == TEMPLATE_ELEMENT) {
this._assertNoComponentsNorElementBindingsOnTemplate(directives, elementProps, events,
element.sourceInfo);
parsedElement =
new EmbeddedTemplateAst(attrs, vars, directives, children, element.sourceInfo);
parsedElement = new EmbeddedTemplateAst(attrs, vars, directives, children,
elementNgContentIndex, element.sourceInfo);
} else {
this._assertOnlyOneComponent(directives, element.sourceInfo);
parsedElement = new ElementAst(attrs, elementProps, events, vars, directives, children,
element.sourceInfo);
parsedElement = new ElementAst(nodeName, attrs, elementProps, events, vars, directives,
children, elementNgContentIndex, element.sourceInfo);
}
if (hasInlineTemplates) {
var templateCssSelector =
this._createElementCssSelector(TEMPLATE_ELEMENT, templateMatchableAttrs);
var templateDirectives = this._createDirectiveAsts(
element.name,
this._parseDirectives(this.selectorMatcher, TEMPLATE_ELEMENT, templateMatchableAttrs),
element.name, this._parseDirectives(this.selectorMatcher, templateCssSelector),
templateElementOrDirectiveProps, element.sourceInfo);
var templateElementProps: BoundElementPropertyAst[] = this._createElementPropertyAsts(
element.name, templateElementOrDirectiveProps, templateDirectives);
this._assertNoComponentsNorElementBindingsOnTemplate(templateDirectives, templateElementProps,
[], element.sourceInfo);
parsedElement = new EmbeddedTemplateAst([], templateVars, templateDirectives, [parsedElement],
component.findNgContentIndex(templateCssSelector),
element.sourceInfo);
}
return parsedElement;
@ -349,8 +354,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
sourceInfo));
}
private _parseDirectives(selectorMatcher: SelectorMatcher, elementName: string,
matchableAttrs: string[][]): DirectiveMetadata[] {
private _createElementCssSelector(elementName: string, matchableAttrs: string[][]): CssSelector {
var cssSelector = new CssSelector();
cssSelector.setElement(elementName);
@ -363,8 +367,14 @@ class TemplateParseVisitor implements HtmlAstVisitor {
classes.forEach(className => cssSelector.addClassName(className));
}
}
return cssSelector;
}
private _parseDirectives(selectorMatcher: SelectorMatcher,
elementCssSelector: CssSelector): DirectiveMetadata[] {
var directives = [];
selectorMatcher.match(cssSelector, (selector, directive) => { directives.push(directive); });
selectorMatcher.match(elementCssSelector,
(selector, directive) => { directives.push(directive); });
// Need to sort the directives so that we get consistent results throughout,
// as selectorMatcher uses Maps inside.
// Also need to make components the first directive in the array
@ -516,9 +526,10 @@ class TemplateParseVisitor implements HtmlAstVisitor {
}
}
_assertNoComponentsNorElementBindingsOnTemplate(directives: DirectiveAst[],
elementProps: BoundElementPropertyAst[],
events: BoundEventAst[], sourceInfo: string) {
private _assertNoComponentsNorElementBindingsOnTemplate(directives: DirectiveAst[],
elementProps: BoundElementPropertyAst[],
events: BoundEventAst[],
sourceInfo: string) {
var componentTypeNames: string[] = this._findComponentDirectiveNames(directives);
if (componentTypeNames.length > 0) {
this._reportError(
@ -555,4 +566,39 @@ export function splitAtColon(input: string, defaultValues: string[]): string[] {
} else {
return defaultValues;
}
}
}
class Component {
static create(directives: DirectiveAst[]): Component {
if (directives.length === 0 || !directives[0].directive.isComponent) {
return EMPTY_COMPONENT;
}
var matcher = new SelectorMatcher();
var ngContentSelectors = directives[0].directive.template.ngContentSelectors;
var wildcardNgContentIndex = null;
for (var i = 0; i < ngContentSelectors.length; i++) {
var selector = ngContentSelectors[i];
if (StringWrapper.equals(selector, '*')) {
wildcardNgContentIndex = i;
} else {
matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);
}
}
return new Component(matcher, wildcardNgContentIndex);
}
constructor(public ngContentIndexMatcher: SelectorMatcher,
public wildcardNgContentIndex: number) {}
findNgContentIndex(selector: CssSelector): number {
var ngContentIndices = [];
if (isPresent(this.wildcardNgContentIndex)) {
ngContentIndices.push(this.wildcardNgContentIndex);
}
this.ngContentIndexMatcher.match(
selector, (selector, ngContentIndex) => { ngContentIndices.push(ngContentIndex); });
ListWrapper.sort(ngContentIndices);
return ngContentIndices.length > 0 ? ngContentIndices[0] : null;
}
}
var EMPTY_COMPONENT = new Component(new SelectorMatcher(), null);

View File

@ -1,8 +1,9 @@
import {StringWrapper} from 'angular2/src/core/facade/lang';
import {StringWrapper, isBlank} from 'angular2/src/core/facade/lang';
var CAMEL_CASE_REGEXP = /([A-Z])/g;
var DASH_CASE_REGEXP = /-([a-z])/g;
var SINGLE_QUOTE_ESCAPE_STRING_RE = /'|\\|\n/g;
var DOUBLE_QUOTE_ESCAPE_STRING_RE = /"|\\|\n/g;
export function camelCaseToDashCase(input: string): string {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP,
@ -13,3 +14,27 @@ export function dashCaseToCamelCase(input: string): string {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP,
(m) => { return m[1].toUpperCase(); });
}
export function escapeSingleQuoteString(input: string): string {
if (isBlank(input)) {
return null;
}
return `'${escapeString(input, SINGLE_QUOTE_ESCAPE_STRING_RE)}'`;
}
export function escapeDoubleQuoteString(input: string): string {
if (isBlank(input)) {
return null;
}
return `"${escapeString(input, DOUBLE_QUOTE_ESCAPE_STRING_RE)}"`;
}
function escapeString(input: string, re: RegExp): string {
return StringWrapper.replaceAllMapped(input, re, (match) => {
if (match[0] == '\n') {
return '\\n';
} else {
return `\\${match[0]}`;
}
});
}