From bc3f4bc816b1055aa5bee0724519be90ce74e74e Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 21 Oct 2016 11:41:14 -0700 Subject: [PATCH] refactor(compiler): extract BindingParser Needed so that we can parse directive host bindings independent of templates. Part of #11683 --- modules/@angular/compiler/src/parse_util.ts | 2 +- .../src/template_parser/binding_parser.ts | 435 ++++++++++++++ .../src/template_parser/template_parser.ts | 549 +++--------------- 3 files changed, 511 insertions(+), 475 deletions(-) create mode 100644 modules/@angular/compiler/src/template_parser/binding_parser.ts diff --git a/modules/@angular/compiler/src/parse_util.ts b/modules/@angular/compiler/src/parse_util.ts index 43ea774853..eebcc0ecaa 100644 --- a/modules/@angular/compiler/src/parse_util.ts +++ b/modules/@angular/compiler/src/parse_util.ts @@ -35,7 +35,7 @@ export enum ParseErrorLevel { FATAL } -export abstract class ParseError { +export class ParseError { constructor( public span: ParseSourceSpan, public msg: string, public level: ParseErrorLevel = ParseErrorLevel.FATAL) {} diff --git a/modules/@angular/compiler/src/template_parser/binding_parser.ts b/modules/@angular/compiler/src/template_parser/binding_parser.ts new file mode 100644 index 0000000000..4c9199e180 --- /dev/null +++ b/modules/@angular/compiler/src/template_parser/binding_parser.ts @@ -0,0 +1,435 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {SchemaMetadata, SecurityContext} from '@angular/core'; + +import {CompilePipeMetadata} from '../compile_metadata'; +import {AST, ASTWithSource, BindingPipe, EmptyExpr, Interpolation, LiteralPrimitive, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast'; +import {Parser} from '../expression_parser/parser'; +import {isPresent} from '../facade/lang'; +import {InterpolationConfig} from '../ml_parser/interpolation_config'; +import {mergeNsAndName} from '../ml_parser/tags'; +import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; +import {view_utils} from '../private_import_core'; +import {ElementSchemaRegistry} from '../schema/element_schema_registry'; +import {splitAtColon, splitAtPeriod} from '../util'; + +import {BoundElementPropertyAst, BoundEventAst, PropertyBindingType, VariableAst} from './template_ast'; + +const PROPERTY_PARTS_SEPARATOR = '.'; +const ATTRIBUTE_PREFIX = 'attr'; +const CLASS_PREFIX = 'class'; +const STYLE_PREFIX = 'style'; + +const ANIMATE_PROP_PREFIX = 'animate-'; + +/** + * Type of a parsed property + */ +export enum BoundPropertyType { + DEFAULT, + LITERAL_ATTR, + ANIMATION +} + +/** + * Represents a parsed property. + */ +export class BoundProperty { + constructor( + public name: string, public expression: ASTWithSource, public type: BoundPropertyType, + public sourceSpan: ParseSourceSpan) {} + + get isLiteral() { return this.type === BoundPropertyType.LITERAL_ATTR; } + + get isAnimation() { return this.type === BoundPropertyType.ANIMATION; } +} + +/** + * Parses bindings in templates and in the directive host area. + */ +export class BindingParser { + pipesByName: Map = new Map(); + errors: ParseError[] = []; + + constructor( + private _exprParser: Parser, private _interpolationConfig: InterpolationConfig, + protected _schemaRegistry: ElementSchemaRegistry, protected _schemas: SchemaMetadata[], + pipes: CompilePipeMetadata[]) { + pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe)); + } + + createDirectiveHostPropertyAsts( + elementName: string, hostProps: {[key: string]: string}, sourceSpan: ParseSourceSpan, + targetPropertyAsts: BoundElementPropertyAst[]) { + if (hostProps) { + const boundProps: BoundProperty[] = []; + Object.keys(hostProps).forEach(propName => { + const expression = hostProps[propName]; + if (typeof expression === 'string') { + this.parsePropertyBinding(propName, expression, true, sourceSpan, [], boundProps); + } else { + this.reportError( + `Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, + sourceSpan); + } + }); + boundProps.forEach( + (prop) => { targetPropertyAsts.push(this.createElementPropertyAst(elementName, prop)); }); + } + } + + createDirectiveHostEventAsts( + hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan, + targetEventAsts: BoundEventAst[]) { + if (hostListeners) { + Object.keys(hostListeners).forEach(propName => { + const expression = hostListeners[propName]; + if (typeof expression === 'string') { + this.parseEvent(propName, expression, sourceSpan, [], targetEventAsts); + } else { + this.reportError( + `Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, + sourceSpan); + } + }); + } + } + + parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource { + const sourceInfo = sourceSpan.start.toString(); + + try { + const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig); + if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan); + this._checkPipes(ast, sourceSpan); + if (ast && + (ast.ast).expressions.length > view_utils.MAX_INTERPOLATION_VALUES) { + throw new Error( + `Only support at most ${view_utils.MAX_INTERPOLATION_VALUES} interpolation values!`); + } + return ast; + } catch (e) { + this.reportError(`${e}`, sourceSpan); + return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); + } + } + + parseInlineTemplateBinding( + name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], + targetProps: BoundProperty[], targetVars: VariableAst[]) { + const bindings = this._parseTemplateBindings(value, sourceSpan); + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (binding.keyIsVar) { + targetVars.push(new VariableAst(binding.key, binding.name, sourceSpan)); + } else if (isPresent(binding.expression)) { + this._parsePropertyAst( + binding.key, binding.expression, sourceSpan, targetMatchableAttrs, targetProps); + } else { + targetMatchableAttrs.push([binding.key, '']); + this.parseLiteralAttr(binding.key, null, sourceSpan, targetMatchableAttrs, targetProps); + } + } + } + + private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] { + const sourceInfo = sourceSpan.start.toString(); + + try { + const bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo); + this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan); + bindingsResult.templateBindings.forEach((binding) => { + if (isPresent(binding.expression)) { + this._checkPipes(binding.expression, sourceSpan); + } + }); + bindingsResult.warnings.forEach( + (warning) => { this.reportError(warning, sourceSpan, ParseErrorLevel.WARNING); }); + return bindingsResult.templateBindings; + } catch (e) { + this.reportError(`${e}`, sourceSpan); + return []; + } + } + + parseLiteralAttr( + name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], + targetProps: BoundProperty[]) { + if (_isAnimationLabel(name)) { + name = name.substring(1); + if (isPresent(value) && value.length > 0) { + this.reportError( + `Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` + + ` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`, + sourceSpan, ParseErrorLevel.FATAL); + } + this._parseAnimation(name, value, sourceSpan, targetMatchableAttrs, targetProps); + } else { + targetProps.push(new BoundProperty( + name, this._exprParser.wrapLiteralPrimitive(value, ''), BoundPropertyType.LITERAL_ATTR, + sourceSpan)); + } + } + + parsePropertyBinding( + name: string, expression: string, isHost: boolean, sourceSpan: ParseSourceSpan, + targetMatchableAttrs: string[][], targetProps: BoundProperty[]) { + let isAnimationProp = false; + if (name.startsWith(ANIMATE_PROP_PREFIX)) { + isAnimationProp = true; + name = name.substring(ANIMATE_PROP_PREFIX.length); + } else if (_isAnimationLabel(name)) { + isAnimationProp = true; + name = name.substring(1); + } + + if (isAnimationProp) { + this._parseAnimation(name, expression, sourceSpan, targetMatchableAttrs, targetProps); + } else { + this._parsePropertyAst( + name, this._parseBinding(expression, isHost, sourceSpan), sourceSpan, + targetMatchableAttrs, targetProps); + } + } + + parsePropertyInterpolation( + name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], + targetProps: BoundProperty[]): boolean { + const expr = this.parseInterpolation(value, sourceSpan); + if (isPresent(expr)) { + this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps); + return true; + } + return false; + } + + private _parsePropertyAst( + name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan, + targetMatchableAttrs: string[][], targetProps: BoundProperty[]) { + targetMatchableAttrs.push([name, ast.source]); + targetProps.push(new BoundProperty(name, ast, BoundPropertyType.DEFAULT, sourceSpan)); + } + + private _parseAnimation( + name: string, expression: string, sourceSpan: ParseSourceSpan, + targetMatchableAttrs: string[][], targetProps: BoundProperty[]) { + // This will occur when a @trigger is not paired with an expression. + // For animations it is valid to not have an expression since */void + // states will be applied by angular when the element is attached/detached + const ast = this._parseBinding(expression || 'null', false, sourceSpan); + targetMatchableAttrs.push([name, ast.source]); + targetProps.push(new BoundProperty(name, ast, BoundPropertyType.ANIMATION, sourceSpan)); + } + + private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan): + ASTWithSource { + const sourceInfo = sourceSpan.start.toString(); + + try { + const ast = isHostBinding ? + this._exprParser.parseSimpleBinding(value, sourceInfo, this._interpolationConfig) : + this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig); + if (ast) this._reportExpressionParserErrors(ast.errors, sourceSpan); + this._checkPipes(ast, sourceSpan); + return ast; + } catch (e) { + this.reportError(`${e}`, sourceSpan); + return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); + } + } + + createElementPropertyAst(elementName: string, boundProp: BoundProperty): BoundElementPropertyAst { + if (boundProp.isAnimation) { + return new BoundElementPropertyAst( + boundProp.name, PropertyBindingType.Animation, SecurityContext.NONE, boundProp.expression, + null, boundProp.sourceSpan); + } + + let unit: string = null; + let bindingType: PropertyBindingType; + let boundPropertyName: string; + const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR); + let securityContext: SecurityContext; + + if (parts.length === 1) { + var partValue = parts[0]; + boundPropertyName = this._schemaRegistry.getMappedPropName(partValue); + securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName); + bindingType = PropertyBindingType.Property; + this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false); + if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) { + let errorMsg = + `Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`; + if (elementName.indexOf('-') > -1) { + errorMsg += + `\n1. If '${elementName}' is an Angular component and it has '${boundPropertyName}' input, then verify that it is part of this module.` + + `\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.\n`; + } + this.reportError(errorMsg, boundProp.sourceSpan); + } + } else { + if (parts[0] == ATTRIBUTE_PREFIX) { + boundPropertyName = parts[1]; + this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, true); + // NB: For security purposes, use the mapped property name, not the attribute name. + const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName); + securityContext = this._schemaRegistry.securityContext(elementName, mapPropName); + + const nsSeparatorIdx = boundPropertyName.indexOf(':'); + if (nsSeparatorIdx > -1) { + const ns = boundPropertyName.substring(0, nsSeparatorIdx); + const name = boundPropertyName.substring(nsSeparatorIdx + 1); + boundPropertyName = mergeNsAndName(ns, name); + } + + bindingType = PropertyBindingType.Attribute; + } else if (parts[0] == CLASS_PREFIX) { + boundPropertyName = parts[1]; + bindingType = PropertyBindingType.Class; + securityContext = SecurityContext.NONE; + } else if (parts[0] == STYLE_PREFIX) { + unit = parts.length > 2 ? parts[2] : null; + boundPropertyName = parts[1]; + bindingType = PropertyBindingType.Style; + securityContext = SecurityContext.STYLE; + } else { + this.reportError(`Invalid property name '${boundProp.name}'`, boundProp.sourceSpan); + bindingType = null; + securityContext = null; + } + } + + return new BoundElementPropertyAst( + boundPropertyName, bindingType, securityContext, boundProp.expression, unit, + boundProp.sourceSpan); + } + + parseEvent( + name: string, expression: string, sourceSpan: ParseSourceSpan, + targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { + if (_isAnimationLabel(name)) { + name = name.substr(1); + this._parseAnimationEvent(name, expression, sourceSpan, targetEvents); + } else { + this._parseEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents); + } + } + + private _parseAnimationEvent( + name: string, expression: string, sourceSpan: ParseSourceSpan, + targetEvents: BoundEventAst[]) { + const matches = splitAtPeriod(name, [name, '']); + const eventName = matches[0]; + const phase = matches[1].toLowerCase(); + if (phase) { + switch (phase) { + case 'start': + case 'done': + const ast = this._parseAction(expression, sourceSpan); + targetEvents.push(new BoundEventAst(eventName, null, phase, ast, sourceSpan)); + break; + + default: + this.reportError( + `The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`, + sourceSpan); + break; + } + } else { + this.reportError( + `The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`, + sourceSpan); + } + } + + private _parseEvent( + name: string, expression: string, sourceSpan: ParseSourceSpan, + targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { + // long format: 'target: eventName' + const [target, eventName] = splitAtColon(name, [null, name]); + const ast = this._parseAction(expression, sourceSpan); + targetMatchableAttrs.push([name, ast.source]); + targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan)); + // Don't detect directives for event names for now, + // so don't add the event name to the matchableAttrs + } + + private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource { + const sourceInfo = sourceSpan.start.toString(); + + try { + const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig); + if (ast) { + this._reportExpressionParserErrors(ast.errors, sourceSpan); + } + if (!ast || ast.ast instanceof EmptyExpr) { + this.reportError(`Empty expressions are not allowed`, sourceSpan); + return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); + } + this._checkPipes(ast, sourceSpan); + return ast; + } catch (e) { + this.reportError(`${e}`, sourceSpan); + return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); + } + } + + reportError( + message: string, sourceSpan: ParseSourceSpan, + level: ParseErrorLevel = ParseErrorLevel.FATAL) { + this.errors.push(new ParseError(sourceSpan, message, level)); + } + + private _reportExpressionParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) { + for (const error of errors) { + this.reportError(error.message, sourceSpan); + } + } + + private _checkPipes(ast: ASTWithSource, sourceSpan: ParseSourceSpan) { + if (isPresent(ast)) { + const collector = new PipeCollector(); + ast.visit(collector); + collector.pipes.forEach((pipeName) => { + if (!this.pipesByName.has(pipeName)) { + this.reportError(`The pipe '${pipeName}' could not be found`, sourceSpan); + } + }); + } + } + + /** + * @param propName the name of the property / attribute + * @param sourceSpan + * @param isAttr true when binding to an attribute + * @private + */ + private _validatePropertyOrAttributeName( + propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void { + const report = isAttr ? this._schemaRegistry.validateAttribute(propName) : + this._schemaRegistry.validateProperty(propName); + if (report.error) { + this.reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL); + } + } +} + +export class PipeCollector extends RecursiveAstVisitor { + pipes: Set = new Set(); + visitPipe(ast: BindingPipe, context: any): any { + this.pipes.add(ast.name); + ast.exp.visit(this); + this.visitAll(ast.args, context); + return null; + } +} + +function _isAnimationLabel(name: string): boolean { + return name[0] == '@'; +} diff --git a/modules/@angular/compiler/src/template_parser/template_parser.ts b/modules/@angular/compiler/src/template_parser/template_parser.ts index a7091bf65b..aa4d31bb2a 100644 --- a/modules/@angular/compiler/src/template_parser/template_parser.ts +++ b/modules/@angular/compiler/src/template_parser/template_parser.ts @@ -25,8 +25,8 @@ import {ProviderElementContext, ProviderViewContext} from '../provider_analyzer' import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {CssSelector, SelectorMatcher} from '../selector'; import {isStyleUrlResolvable} from '../style_url_resolver'; -import {splitAtColon, splitAtPeriod} from '../util'; +import {BindingParser, BoundProperty} from './binding_parser'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from './template_ast'; import {PreparsedElementType, preparseElement} from './template_preparser'; @@ -56,17 +56,11 @@ const IDENT_BANANA_BOX_IDX = 8; const IDENT_PROPERTY_IDX = 9; const IDENT_EVENT_IDX = 10; -const ANIMATE_PROP_PREFIX = 'animate-'; const TEMPLATE_ELEMENT = 'template'; const TEMPLATE_ATTR = 'template'; const TEMPLATE_ATTR_PREFIX = '*'; const CLASS_ATTR = 'class'; -const PROPERTY_PARTS_SEPARATOR = '.'; -const ATTRIBUTE_PREFIX = 'attr'; -const CLASS_PREFIX = 'class'; -const STYLE_PREFIX = 'style'; - const TEXT_CSS_SELECTOR = CssSelector.parse('*')[0]; /** @@ -135,9 +129,16 @@ export class TemplateParser { const uniqPipes = removeIdentifierDuplicates(pipes); const providerViewContext = new ProviderViewContext(component, htmlAstWithErrors.rootNodes[0].sourceSpan); + let interpolationConfig: InterpolationConfig; + if (component.template && component.template.interpolation) { + interpolationConfig = { + start: component.template.interpolation[0], + end: component.template.interpolation[1] + }; + } const parseVisitor = new TemplateParseVisitor( - providerViewContext, uniqDirectives, uniqPipes, schemas, this._exprParser, - this._schemaRegistry); + providerViewContext, uniqDirectives, uniqPipes, this._exprParser, interpolationConfig, + this._schemaRegistry, schemas); result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT); errors.push(...parseVisitor.errors, ...providerViewContext.errors); } else { @@ -195,135 +196,23 @@ export class TemplateParser { } } -class TemplateParseVisitor implements html.Visitor { +class TemplateParseVisitor extends BindingParser implements html.Visitor { selectorMatcher = new SelectorMatcher(); errors: TemplateParseError[] = []; directivesIndex = new Map(); ngContentCount: number = 0; - pipesByName: Map = new Map(); - - private _interpolationConfig: InterpolationConfig; constructor( public providerViewContext: ProviderViewContext, directives: CompileDirectiveMetadata[], - pipes: CompilePipeMetadata[], private _schemas: SchemaMetadata[], private _exprParser: Parser, - private _schemaRegistry: ElementSchemaRegistry) { - const tempMeta = providerViewContext.component.template; - - if (tempMeta && tempMeta.interpolation) { - this._interpolationConfig = { - start: tempMeta.interpolation[0], - end: tempMeta.interpolation[1] - }; - } + pipes: CompilePipeMetadata[], _exprParser: Parser, interpolationConfig: InterpolationConfig, + _schemaRegistry: ElementSchemaRegistry, schemas: SchemaMetadata[]) { + super(_exprParser, interpolationConfig, _schemaRegistry, schemas, pipes); directives.forEach((directive: CompileDirectiveMetadata, index: number) => { const selector = CssSelector.parse(directive.selector); this.selectorMatcher.addSelectables(selector, directive); this.directivesIndex.set(directive, index); }); - - pipes.forEach(pipe => this.pipesByName.set(pipe.name, pipe)); - } - - private _reportError( - message: string, sourceSpan: ParseSourceSpan, - level: ParseErrorLevel = ParseErrorLevel.FATAL) { - this.errors.push(new TemplateParseError(message, sourceSpan, level)); - } - - private _reportParserErrors(errors: ParserError[], sourceSpan: ParseSourceSpan) { - for (const error of errors) { - this._reportError(error.message, sourceSpan); - } - } - - private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource { - const sourceInfo = sourceSpan.start.toString(); - - try { - const ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig); - if (ast) this._reportParserErrors(ast.errors, sourceSpan); - this._checkPipes(ast, sourceSpan); - if (isPresent(ast) && - (ast.ast).expressions.length > view_utils.MAX_INTERPOLATION_VALUES) { - throw new Error( - `Only support at most ${view_utils.MAX_INTERPOLATION_VALUES} interpolation values!`); - } - return ast; - } catch (e) { - this._reportError(`${e}`, sourceSpan); - return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); - } - } - - private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource { - const sourceInfo = sourceSpan.start.toString(); - - try { - const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig); - if (ast) { - this._reportParserErrors(ast.errors, sourceSpan); - } - if (!ast || ast.ast instanceof EmptyExpr) { - this._reportError(`Empty expressions are not allowed`, sourceSpan); - return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); - } - this._checkPipes(ast, sourceSpan); - return ast; - } catch (e) { - this._reportError(`${e}`, sourceSpan); - return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); - } - } - - private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan): - ASTWithSource { - const sourceInfo = sourceSpan.start.toString(); - - try { - const ast = isHostBinding ? - this._exprParser.parseSimpleBinding(value, sourceInfo, this._interpolationConfig) : - this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig); - if (ast) this._reportParserErrors(ast.errors, sourceSpan); - this._checkPipes(ast, sourceSpan); - return ast; - } catch (e) { - this._reportError(`${e}`, sourceSpan); - return this._exprParser.wrapLiteralPrimitive('ERROR', sourceInfo); - } - } - - private _parseTemplateBindings(value: string, sourceSpan: ParseSourceSpan): TemplateBinding[] { - const sourceInfo = sourceSpan.start.toString(); - - try { - const bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo); - this._reportParserErrors(bindingsResult.errors, sourceSpan); - bindingsResult.templateBindings.forEach((binding) => { - if (isPresent(binding.expression)) { - this._checkPipes(binding.expression, sourceSpan); - } - }); - bindingsResult.warnings.forEach( - (warning) => { this._reportError(warning, sourceSpan, ParseErrorLevel.WARNING); }); - return bindingsResult.templateBindings; - } catch (e) { - this._reportError(`${e}`, sourceSpan); - return []; - } - } - - private _checkPipes(ast: ASTWithSource, sourceSpan: ParseSourceSpan) { - if (isPresent(ast)) { - const collector = new PipeCollector(); - ast.visit(collector); - collector.pipes.forEach((pipeName) => { - if (!this.pipesByName.has(pipeName)) { - this._reportError(`The pipe '${pipeName}' could not be found`, sourceSpan); - } - }); - } } visitExpansion(expansion: html.Expansion, context: any): any { return null; } @@ -332,7 +221,7 @@ class TemplateParseVisitor implements html.Visitor { visitText(text: html.Text, parent: ElementContext): any { const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR); - const expr = this._parseInterpolation(text.value, text.sourceSpan); + const expr = this.parseInterpolation(text.value, text.sourceSpan); if (isPresent(expr)) { return new BoundTextAst(expr, ngContentIndex, text.sourceSpan); } else { @@ -364,13 +253,12 @@ class TemplateParseVisitor implements html.Visitor { } const matchableAttrs: string[][] = []; - const elementOrDirectiveProps: BoundElementOrDirectiveProperty[] = []; + const elementOrDirectiveProps: BoundProperty[] = []; const elementOrDirectiveRefs: ElementOrDirectiveRef[] = []; const elementVars: VariableAst[] = []; - const animationProps: BoundElementPropertyAst[] = []; const events: BoundEventAst[] = []; - const templateElementOrDirectiveProps: BoundElementOrDirectiveProperty[] = []; + const templateElementOrDirectiveProps: BoundProperty[] = []; const templateMatchableAttrs: string[][] = []; const templateElementVars: VariableAst[] = []; @@ -381,16 +269,27 @@ class TemplateParseVisitor implements html.Visitor { element.attrs.forEach(attr => { const hasBinding = this._parseAttr( - isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, animationProps, events, + isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events, elementOrDirectiveRefs, elementVars); - const hasTemplateBinding = this._parseInlineTemplateBinding( - attr, templateMatchableAttrs, templateElementOrDirectiveProps, templateElementVars); - - if (hasTemplateBinding && hasInlineTemplates) { - this._reportError( - `Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with *`, - attr.sourceSpan); + let templateBindingsSource: string; + if (this._normalizeAttributeName(attr.name) == TEMPLATE_ATTR) { + templateBindingsSource = attr.value; + } else if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) { + const key = attr.name.substring(TEMPLATE_ATTR_PREFIX.length); // remove the star + templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value; + } + const hasTemplateBinding = isPresent(templateBindingsSource); + if (hasTemplateBinding) { + if (hasInlineTemplates) { + this.reportError( + `Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with *`, + attr.sourceSpan); + } + hasInlineTemplates = true; + this.parseInlineTemplateBinding( + attr.name, templateBindingsSource, attr.sourceSpan, templateMatchableAttrs, + templateElementOrDirectiveProps, templateElementVars); } if (!hasBinding && !hasTemplateBinding) { @@ -398,10 +297,6 @@ class TemplateParseVisitor implements html.Visitor { attrs.push(this.visitAttribute(attr, null)); matchableAttrs.push([attr.name, attr.value]); } - - if (hasTemplateBinding) { - hasInlineTemplates = true; - } }); const elementCssSelector = createElementCssSelector(nodeName, matchableAttrs); @@ -412,8 +307,7 @@ class TemplateParseVisitor implements html.Visitor { isTemplateElement, element.name, directiveMetas, elementOrDirectiveProps, elementOrDirectiveRefs, element.sourceSpan, references); const elementProps: BoundElementPropertyAst[] = - this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts) - .concat(animationProps); + this._createElementPropertyAsts(element.name, elementOrDirectiveProps, directiveAsts); const isViewRoot = parent.isTemplateElement || hasInlineTemplates; const providerContext = new ProviderElementContext( this.providerViewContext, parent.providerContext, isViewRoot, directiveAsts, attrs, @@ -433,7 +327,7 @@ class TemplateParseVisitor implements html.Visitor { if (preparsedElement.type === PreparsedElementType.NG_CONTENT) { if (element.children && !element.children.every(_isEmptyTextNode)) { - this._reportError(` element cannot have content.`, element.sourceSpan); + this.reportError(` element cannot have content.`, element.sourceSpan); } parsedElement = new NgContentAst( @@ -506,7 +400,7 @@ class TemplateParseVisitor implements html.Visitor { animationInputs.forEach(input => { const name = input.name; if (!triggerLookup.has(name)) { - this._reportError(`Couldn't find an animation entry for "${name}"`, input.sourceSpan); + this.reportError(`Couldn't find an animation entry for "${name}"`, input.sourceSpan); } }); @@ -514,7 +408,7 @@ class TemplateParseVisitor implements html.Visitor { if (output.isAnimation) { const found = animationInputs.find(input => input.name == output.name); if (!found) { - this._reportError( + this.reportError( `Unable to listen on (@${output.name}.${output.phase}) because the animation trigger [@${output.name}] isn't being used on the same element`, output.sourceSpan); } @@ -522,39 +416,9 @@ class TemplateParseVisitor implements html.Visitor { }); } - private _parseInlineTemplateBinding( - attr: html.Attribute, targetMatchableAttrs: string[][], - targetProps: BoundElementOrDirectiveProperty[], targetVars: VariableAst[]): boolean { - let templateBindingsSource: string = null; - if (this._normalizeAttributeName(attr.name) == TEMPLATE_ATTR) { - templateBindingsSource = attr.value; - } else if (attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) { - const key = attr.name.substring(TEMPLATE_ATTR_PREFIX.length); // remove the star - templateBindingsSource = (attr.value.length == 0) ? key : key + ' ' + attr.value; - } - if (isPresent(templateBindingsSource)) { - const bindings = this._parseTemplateBindings(templateBindingsSource, attr.sourceSpan); - for (let i = 0; i < bindings.length; i++) { - const binding = bindings[i]; - if (binding.keyIsVar) { - targetVars.push(new VariableAst(binding.key, binding.name, attr.sourceSpan)); - } else if (isPresent(binding.expression)) { - this._parsePropertyAst( - binding.key, binding.expression, attr.sourceSpan, targetMatchableAttrs, targetProps); - } else { - targetMatchableAttrs.push([binding.key, '']); - this._parseLiteralAttr(binding.key, null, attr.sourceSpan, targetProps); - } - } - return true; - } - return false; - } - private _parseAttr( isTemplateElement: boolean, attr: html.Attribute, targetMatchableAttrs: string[][], - targetProps: BoundElementOrDirectiveProperty[], - targetAnimationProps: BoundElementPropertyAst[], targetEvents: BoundEventAst[], + targetProps: BoundProperty[], targetEvents: BoundEventAst[], targetRefs: ElementOrDirectiveRef[], targetVars: VariableAst[]): boolean { const name = this._normalizeAttributeName(attr.name); const value = attr.value; @@ -566,16 +430,15 @@ class TemplateParseVisitor implements html.Visitor { if (bindParts !== null) { hasBinding = true; if (isPresent(bindParts[KW_BIND_IDX])) { - this._parsePropertyOrAnimation( - bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetProps, - targetAnimationProps); + this.parsePropertyBinding( + bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps); } else if (bindParts[KW_LET_IDX]) { if (isTemplateElement) { const identifier = bindParts[IDENT_KW_IDX]; this._parseVariable(identifier, value, srcSpan, targetVars); } else { - this._reportError(`"let-" is only supported on template elements.`, srcSpan); + this.reportError(`"let-" is only supported on template elements.`, srcSpan); } } else if (bindParts[KW_REF_IDX]) { @@ -583,48 +446,41 @@ class TemplateParseVisitor implements html.Visitor { this._parseReference(identifier, value, srcSpan, targetRefs); } else if (bindParts[KW_ON_IDX]) { - this._parseEventOrAnimationEvent( + this.parseEvent( bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); } else if (bindParts[KW_BINDON_IDX]) { - this._parsePropertyOrAnimation( - bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetProps, - targetAnimationProps); + this.parsePropertyBinding( + bindParts[IDENT_KW_IDX], value, false, srcSpan, targetMatchableAttrs, targetProps); this._parseAssignmentEvent( bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); } else if (bindParts[KW_AT_IDX]) { - if (_isAnimationLabel(name) && isPresent(value) && value.length > 0) { - this._reportError( - `Assigning animation triggers via @prop="exp" attributes with an expression is invalid.` + - ` Use property bindings (e.g. [@prop]="exp") or use an attribute without a value (e.g. @prop) instead.`, - srcSpan, ParseErrorLevel.FATAL); - } - this._parseAnimation( - bindParts[IDENT_KW_IDX], value, srcSpan, targetMatchableAttrs, targetAnimationProps); + this.parseLiteralAttr(name, value, srcSpan, targetMatchableAttrs, targetProps); + } else if (bindParts[IDENT_BANANA_BOX_IDX]) { - this._parsePropertyOrAnimation( - bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetProps, - targetAnimationProps); + this.parsePropertyBinding( + bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, targetMatchableAttrs, + targetProps); this._parseAssignmentEvent( bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); } else if (bindParts[IDENT_PROPERTY_IDX]) { - this._parsePropertyOrAnimation( - bindParts[IDENT_PROPERTY_IDX], value, srcSpan, targetMatchableAttrs, targetProps, - targetAnimationProps); + this.parsePropertyBinding( + bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, targetMatchableAttrs, + targetProps); } else if (bindParts[IDENT_EVENT_IDX]) { - this._parseEventOrAnimationEvent( + this.parseEvent( bindParts[IDENT_EVENT_IDX], value, srcSpan, targetMatchableAttrs, targetEvents); } } else { hasBinding = - this._parsePropertyInterpolation(name, value, srcSpan, targetMatchableAttrs, targetProps); + this.parsePropertyInterpolation(name, value, srcSpan, targetMatchableAttrs, targetProps); } if (!hasBinding) { - this._parseLiteralAttr(name, value, srcSpan, targetProps); + this.parseLiteralAttr(name, value, srcSpan, targetMatchableAttrs, targetProps); } return hasBinding; @@ -637,7 +493,7 @@ class TemplateParseVisitor implements html.Visitor { private _parseVariable( identifier: string, value: string, sourceSpan: ParseSourceSpan, targetVars: VariableAst[]) { if (identifier.indexOf('-') > -1) { - this._reportError(`"-" is not allowed in variable names`, sourceSpan); + this.reportError(`"-" is not allowed in variable names`, sourceSpan); } targetVars.push(new VariableAst(identifier, value, sourceSpan)); @@ -647,133 +503,19 @@ class TemplateParseVisitor implements html.Visitor { identifier: string, value: string, sourceSpan: ParseSourceSpan, targetRefs: ElementOrDirectiveRef[]) { if (identifier.indexOf('-') > -1) { - this._reportError(`"-" is not allowed in reference names`, sourceSpan); + this.reportError(`"-" is not allowed in reference names`, sourceSpan); } targetRefs.push(new ElementOrDirectiveRef(identifier, value, sourceSpan)); } - private _parsePropertyOrAnimation( - name: string, expression: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[], - targetAnimationProps: BoundElementPropertyAst[]) { - const animatePropLength = ANIMATE_PROP_PREFIX.length; - var isAnimationProp = _isAnimationLabel(name); - var animationPrefixLength = 1; - if (name.substring(0, animatePropLength) == ANIMATE_PROP_PREFIX) { - isAnimationProp = true; - animationPrefixLength = animatePropLength; - } - - if (isAnimationProp) { - this._parseAnimation( - name.substr(animationPrefixLength), expression, sourceSpan, targetMatchableAttrs, - targetAnimationProps); - } else { - this._parsePropertyAst( - name, this._parseBinding(expression, false, sourceSpan), sourceSpan, targetMatchableAttrs, - targetProps); - } - } - - private _parseAnimation( - name: string, expression: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetAnimationProps: BoundElementPropertyAst[]) { - // This will occur when a @trigger is not paired with an expression. - // For animations it is valid to not have an expression since */void - // states will be applied by angular when the element is attached/detached - if (!isPresent(expression) || expression.length == 0) { - expression = 'null'; - } - - const ast = this._parseBinding(expression, false, sourceSpan); - targetMatchableAttrs.push([name, ast.source]); - targetAnimationProps.push(new BoundElementPropertyAst( - name, PropertyBindingType.Animation, SecurityContext.NONE, ast, null, sourceSpan)); - } - - private _parsePropertyInterpolation( - name: string, value: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], - targetProps: BoundElementOrDirectiveProperty[]): boolean { - const expr = this._parseInterpolation(value, sourceSpan); - if (isPresent(expr)) { - this._parsePropertyAst(name, expr, sourceSpan, targetMatchableAttrs, targetProps); - return true; - } - return false; - } - - private _parsePropertyAst( - name: string, ast: ASTWithSource, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetProps: BoundElementOrDirectiveProperty[]) { - targetMatchableAttrs.push([name, ast.source]); - targetProps.push(new BoundElementOrDirectiveProperty(name, ast, false, sourceSpan)); - } - private _parseAssignmentEvent( name: string, expression: string, sourceSpan: ParseSourceSpan, targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { - this._parseEventOrAnimationEvent( + this.parseEvent( `${name}Change`, `${expression}=$event`, sourceSpan, targetMatchableAttrs, targetEvents); } - private _parseEventOrAnimationEvent( - name: string, expression: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { - if (_isAnimationLabel(name)) { - name = name.substr(1); - this._parseAnimationEvent(name, expression, sourceSpan, targetEvents); - } else { - this._parseEvent(name, expression, sourceSpan, targetMatchableAttrs, targetEvents); - } - } - - private _parseAnimationEvent( - name: string, expression: string, sourceSpan: ParseSourceSpan, - targetEvents: BoundEventAst[]) { - const matches = splitAtPeriod(name, [name, '']); - const eventName = matches[0]; - const phase = matches[1].toLowerCase(); - if (phase) { - switch (phase) { - case 'start': - case 'done': - const ast = this._parseAction(expression, sourceSpan); - targetEvents.push(new BoundEventAst(eventName, null, phase, ast, sourceSpan)); - break; - - default: - this._reportError( - `The provided animation output phase value "${phase}" for "@${eventName}" is not supported (use start or done)`, - sourceSpan); - break; - } - } else { - this._reportError( - `The animation trigger output event (@${eventName}) is missing its phase value name (start or done are currently supported)`, - sourceSpan); - } - } - - private _parseEvent( - name: string, expression: string, sourceSpan: ParseSourceSpan, - targetMatchableAttrs: string[][], targetEvents: BoundEventAst[]) { - // long format: 'target: eventName' - const [target, eventName] = splitAtColon(name, [null, name]); - const ast = this._parseAction(expression, sourceSpan); - targetMatchableAttrs.push([name, ast.source]); - targetEvents.push(new BoundEventAst(eventName, target, null, ast, sourceSpan)); - // Don't detect directives for event names for now, - // so don't add the event name to the matchableAttrs - } - - private _parseLiteralAttr( - name: string, value: string, sourceSpan: ParseSourceSpan, - targetProps: BoundElementOrDirectiveProperty[]) { - targetProps.push(new BoundElementOrDirectiveProperty( - name, this._exprParser.wrapLiteralPrimitive(value, ''), true, sourceSpan)); - } - private _parseDirectives(selectorMatcher: SelectorMatcher, elementCssSelector: CssSelector): {directives: CompileDirectiveMetadata[], matchElement: boolean} { // Need to sort the directives so that we get consistent results throughout, @@ -796,7 +538,7 @@ class TemplateParseVisitor implements html.Visitor { private _createDirectiveAsts( isTemplateElement: boolean, elementName: string, directives: CompileDirectiveMetadata[], - props: BoundElementOrDirectiveProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[], + props: BoundProperty[], elementOrDirectiveRefs: ElementOrDirectiveRef[], elementSourceSpan: ParseSourceSpan, targetReferences: ReferenceAst[]): DirectiveAst[] { const matchedReferences = new Set(); let component: CompileDirectiveMetadata = null; @@ -809,9 +551,9 @@ class TemplateParseVisitor implements html.Visitor { const hostProperties: BoundElementPropertyAst[] = []; const hostEvents: BoundEventAst[] = []; const directiveProperties: BoundDirectivePropertyAst[] = []; - this._createDirectiveHostPropertyAsts( + this.createDirectiveHostPropertyAsts( elementName, directive.hostProperties, sourceSpan, hostProperties); - this._createDirectiveHostEventAsts(directive.hostListeners, sourceSpan, hostEvents); + this.createDirectiveHostEventAsts(directive.hostListeners, sourceSpan, hostEvents); this._createDirectivePropertyAsts(directive.inputs, props, directiveProperties); elementOrDirectiveRefs.forEach((elOrDirRef) => { if ((elOrDirRef.value.length === 0 && directive.isComponent) || @@ -827,7 +569,7 @@ class TemplateParseVisitor implements html.Visitor { elementOrDirectiveRefs.forEach((elOrDirRef) => { if (elOrDirRef.value.length > 0) { if (!matchedReferences.has(elOrDirRef.name)) { - this._reportError( + this.reportError( `There is no directive with "exportAs" set to "${elOrDirRef.value}"`, elOrDirRef.sourceSpan); } @@ -842,47 +584,11 @@ class TemplateParseVisitor implements html.Visitor { return directiveAsts; } - private _createDirectiveHostPropertyAsts( - elementName: string, hostProps: {[key: string]: string}, sourceSpan: ParseSourceSpan, - targetPropertyAsts: BoundElementPropertyAst[]) { - if (hostProps) { - Object.keys(hostProps).forEach(propName => { - const expression = hostProps[propName]; - if (typeof expression === 'string') { - const exprAst = this._parseBinding(expression, true, sourceSpan); - targetPropertyAsts.push( - this._createElementPropertyAst(elementName, propName, exprAst, sourceSpan)); - } else { - this._reportError( - `Value of the host property binding "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, - sourceSpan); - } - }); - } - } - - private _createDirectiveHostEventAsts( - hostListeners: {[key: string]: string}, sourceSpan: ParseSourceSpan, - targetEventAsts: BoundEventAst[]) { - if (hostListeners) { - Object.keys(hostListeners).forEach(propName => { - const expression = hostListeners[propName]; - if (typeof expression === 'string') { - this._parseEventOrAnimationEvent(propName, expression, sourceSpan, [], targetEventAsts); - } else { - this._reportError( - `Value of the host listener "${propName}" needs to be a string representing an expression but got "${expression}" (${typeof expression})`, - sourceSpan); - } - }); - } - } - private _createDirectivePropertyAsts( - directiveProperties: {[key: string]: string}, boundProps: BoundElementOrDirectiveProperty[], + directiveProperties: {[key: string]: string}, boundProps: BoundProperty[], targetBoundDirectiveProps: BoundDirectivePropertyAst[]) { if (directiveProperties) { - const boundPropsByName = new Map(); + const boundPropsByName = new Map(); boundProps.forEach(boundProp => { const prevValue = boundPropsByName.get(boundProp.name); if (!prevValue || prevValue.isLiteral) { @@ -905,7 +611,7 @@ class TemplateParseVisitor implements html.Visitor { } private _createElementPropertyAsts( - elementName: string, props: BoundElementOrDirectiveProperty[], + elementName: string, props: BoundProperty[], directives: DirectiveAst[]): BoundElementPropertyAst[] { const boundElementProps: BoundElementPropertyAst[] = []; const boundDirectivePropsIndex = new Map(); @@ -916,98 +622,14 @@ class TemplateParseVisitor implements html.Visitor { }); }); - props.forEach((prop: BoundElementOrDirectiveProperty) => { + props.forEach((prop: BoundProperty) => { if (!prop.isLiteral && !boundDirectivePropsIndex.get(prop.name)) { - boundElementProps.push(this._createElementPropertyAst( - elementName, prop.name, prop.expression, prop.sourceSpan)); + boundElementProps.push(this.createElementPropertyAst(elementName, prop)); } }); return boundElementProps; } - private _createElementPropertyAst( - elementName: string, name: string, ast: AST, - sourceSpan: ParseSourceSpan): BoundElementPropertyAst { - let unit: string = null; - let bindingType: PropertyBindingType; - let boundPropertyName: string; - const parts = name.split(PROPERTY_PARTS_SEPARATOR); - let securityContext: SecurityContext; - - if (parts.length === 1) { - var partValue = parts[0]; - if (_isAnimationLabel(partValue)) { - boundPropertyName = partValue.substr(1); - bindingType = PropertyBindingType.Animation; - securityContext = SecurityContext.NONE; - } else { - boundPropertyName = this._schemaRegistry.getMappedPropName(partValue); - securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName); - bindingType = PropertyBindingType.Property; - this._validatePropertyOrAttributeName(boundPropertyName, sourceSpan, false); - if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) { - let errorMsg = - `Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`; - if (elementName.indexOf('-') > -1) { - errorMsg += - `\n1. If '${elementName}' is an Angular component and it has '${boundPropertyName}' input, then verify that it is part of this module.` + - `\n2. If '${elementName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.\n`; - } - this._reportError(errorMsg, sourceSpan); - } - } - } else { - if (parts[0] == ATTRIBUTE_PREFIX) { - boundPropertyName = parts[1]; - this._validatePropertyOrAttributeName(boundPropertyName, sourceSpan, true); - // NB: For security purposes, use the mapped property name, not the attribute name. - const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName); - securityContext = this._schemaRegistry.securityContext(elementName, mapPropName); - - const nsSeparatorIdx = boundPropertyName.indexOf(':'); - if (nsSeparatorIdx > -1) { - const ns = boundPropertyName.substring(0, nsSeparatorIdx); - const name = boundPropertyName.substring(nsSeparatorIdx + 1); - boundPropertyName = mergeNsAndName(ns, name); - } - - bindingType = PropertyBindingType.Attribute; - } else if (parts[0] == CLASS_PREFIX) { - boundPropertyName = parts[1]; - bindingType = PropertyBindingType.Class; - securityContext = SecurityContext.NONE; - } else if (parts[0] == STYLE_PREFIX) { - unit = parts.length > 2 ? parts[2] : null; - boundPropertyName = parts[1]; - bindingType = PropertyBindingType.Style; - securityContext = SecurityContext.STYLE; - } else { - this._reportError(`Invalid property name '${name}'`, sourceSpan); - bindingType = null; - securityContext = null; - } - } - - return new BoundElementPropertyAst( - boundPropertyName, bindingType, securityContext, ast, unit, sourceSpan); - } - - - /** - * @param propName the name of the property / attribute - * @param sourceSpan - * @param isAttr true when binding to an attribute - * @private - */ - private _validatePropertyOrAttributeName( - propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void { - const report = isAttr ? this._schemaRegistry.validateAttribute(propName) : - this._schemaRegistry.validateProperty(propName); - if (report.error) { - this._reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL); - } - } - private _findComponentDirectives(directives: DirectiveAst[]): DirectiveAst[] { return directives.filter(directive => directive.directive.isComponent); } @@ -1020,7 +642,7 @@ class TemplateParseVisitor implements html.Visitor { private _assertOnlyOneComponent(directives: DirectiveAst[], sourceSpan: ParseSourceSpan) { const componentTypeNames = this._findComponentDirectiveNames(directives); if (componentTypeNames.length > 1) { - this._reportError(`More than one component: ${componentTypeNames.join(',')}`, sourceSpan); + this.reportError(`More than one component: ${componentTypeNames.join(',')}`, sourceSpan); } } @@ -1040,7 +662,7 @@ class TemplateParseVisitor implements html.Visitor { const errorMsg = `'${elName}' is not a known element:\n` + `1. If '${elName}' is an Angular component, then verify that it is part of this module.\n` + `2. If '${elName}' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.`; - this._reportError(errorMsg, element.sourceSpan); + this.reportError(errorMsg, element.sourceSpan); } } @@ -1049,11 +671,11 @@ class TemplateParseVisitor implements html.Visitor { sourceSpan: ParseSourceSpan) { const componentTypeNames: string[] = this._findComponentDirectiveNames(directives); if (componentTypeNames.length > 0) { - this._reportError( + this.reportError( `Components on an embedded template: ${componentTypeNames.join(',')}`, sourceSpan); } elementProps.forEach(prop => { - this._reportError( + this.reportError( `Property binding ${prop.name} not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "directives" section.`, sourceSpan); }); @@ -1072,7 +694,7 @@ class TemplateParseVisitor implements html.Visitor { events.forEach(event => { if (isPresent(event.target) || !allDirectiveEvents.has(event.name)) { - this._reportError( + this.reportError( `Event binding ${event.fullName} not emitted by any directive on an embedded template. Make sure that the event name is spelled correctly and all directives are listed in the "directives" section.`, event.sourceSpan); } @@ -1116,12 +738,6 @@ class NonBindableVisitor implements html.Visitor { visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; } } -class BoundElementOrDirectiveProperty { - constructor( - public name: string, public expression: AST, public isLiteral: boolean, - public sourceSpan: ParseSourceSpan) {} -} - class ElementOrDirectiveRef { constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} } @@ -1189,21 +805,6 @@ function createElementCssSelector(elementName: string, matchableAttrs: string[][ const EMPTY_ELEMENT_CONTEXT = new ElementContext(true, new SelectorMatcher(), null, null); const NON_BINDABLE_VISITOR = new NonBindableVisitor(); - -export class PipeCollector extends RecursiveAstVisitor { - pipes: Set = new Set(); - visitPipe(ast: BindingPipe, context: any): any { - this.pipes.add(ast.name); - ast.exp.visit(this); - this.visitAll(ast.args, context); - return null; - } -} - -function _isAnimationLabel(name: string): boolean { - return name[0] == '@'; -} - function _isEmptyTextNode(node: html.Node): boolean { return node instanceof html.Text && node.value.trim().length == 0; } \ No newline at end of file