2016-10-21 11:41:14 -07:00
/ * *
* @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-' ;
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 < string , CompilePipeMetadata > = new Map ( ) ;
constructor (
private _exprParser : Parser , private _interpolationConfig : InterpolationConfig ,
2016-10-24 11:11:31 -07:00
private _schemaRegistry : ElementSchemaRegistry , private _schemas : SchemaMetadata [ ] ,
pipes : CompilePipeMetadata [ ] , private _targetErrors : ParseError [ ] ) {
2016-10-21 11:41:14 -07:00
pipes . forEach ( pipe = > this . pipesByName . set ( pipe . name , pipe ) ) ;
}
createDirectiveHostPropertyAsts (
2016-10-24 11:11:31 -07:00
elementName : string , hostProps : { [ key : string ] : string } ,
sourceSpan : ParseSourceSpan ) : BoundElementPropertyAst [ ] {
2016-10-21 11:41:14 -07:00
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 {
2016-10-24 11:11:31 -07:00
this . _reportError (
2016-10-21 11:41:14 -07:00
` Value of the host property binding " ${ propName } " needs to be a string representing an expression but got " ${ expression } " ( ${ typeof expression } ) ` ,
sourceSpan ) ;
}
} ) ;
2016-10-24 11:11:31 -07:00
return boundProps . map ( ( prop ) = > this . createElementPropertyAst ( elementName , prop ) ) ;
2016-10-21 11:41:14 -07:00
}
}
2016-10-24 11:11:31 -07:00
createDirectiveHostEventAsts ( hostListeners : { [ key : string ] : string } , sourceSpan : ParseSourceSpan ) :
BoundEventAst [ ] {
2016-10-21 11:41:14 -07:00
if ( hostListeners ) {
2016-10-24 11:11:31 -07:00
const targetEventAsts : BoundEventAst [ ] = [ ] ;
2016-10-21 11:41:14 -07:00
Object . keys ( hostListeners ) . forEach ( propName = > {
const expression = hostListeners [ propName ] ;
if ( typeof expression === 'string' ) {
this . parseEvent ( propName , expression , sourceSpan , [ ] , targetEventAsts ) ;
} else {
2016-10-24 11:11:31 -07:00
this . _reportError (
2016-10-21 11:41:14 -07:00
` Value of the host listener " ${ propName } " needs to be a string representing an expression but got " ${ expression } " ( ${ typeof expression } ) ` ,
sourceSpan ) ;
}
} ) ;
2016-10-24 11:11:31 -07:00
return targetEventAsts ;
2016-10-21 11:41:14 -07:00
}
}
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 &&
( < Interpolation > 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 ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( ` ${ e } ` , sourceSpan ) ;
2016-10-21 11:41:14 -07:00
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 (
2016-10-24 11:11:31 -07:00
( warning ) = > { this . _reportError ( warning , sourceSpan , ParseErrorLevel . WARNING ) ; } ) ;
2016-10-21 11:41:14 -07:00
return bindingsResult . templateBindings ;
} catch ( e ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( ` ${ e } ` , sourceSpan ) ;
2016-10-21 11:41:14 -07:00
return [ ] ;
}
}
parseLiteralAttr (
name : string , value : string , sourceSpan : ParseSourceSpan , targetMatchableAttrs : string [ ] [ ] ,
targetProps : BoundProperty [ ] ) {
if ( _isAnimationLabel ( name ) ) {
name = name . substring ( 1 ) ;
2016-10-24 11:11:31 -07:00
if ( value ) {
this . _reportError (
2016-10-21 11:41:14 -07:00
` 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 ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( ` ${ e } ` , sourceSpan ) ;
2016-10-21 11:41:14 -07:00
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 ` ;
}
2016-10-24 11:11:31 -07:00
this . _reportError ( errorMsg , boundProp . sourceSpan ) ;
2016-10-21 11:41:14 -07:00
}
} 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 {
2016-10-24 11:11:31 -07:00
this . _reportError ( ` Invalid property name ' ${ boundProp . name } ' ` , boundProp . sourceSpan ) ;
2016-10-21 11:41:14 -07:00
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 :
2016-10-24 11:11:31 -07:00
this . _reportError (
2016-10-21 11:41:14 -07:00
` The provided animation output phase value " ${ phase } " for "@ ${ eventName } " is not supported (use start or done) ` ,
sourceSpan ) ;
break ;
}
} else {
2016-10-24 11:11:31 -07:00
this . _reportError (
2016-10-21 11:41:14 -07:00
` 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 ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( ` Empty expressions are not allowed ` , sourceSpan ) ;
2016-10-21 11:41:14 -07:00
return this . _exprParser . wrapLiteralPrimitive ( 'ERROR' , sourceInfo ) ;
}
this . _checkPipes ( ast , sourceSpan ) ;
return ast ;
} catch ( e ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( ` ${ e } ` , sourceSpan ) ;
2016-10-21 11:41:14 -07:00
return this . _exprParser . wrapLiteralPrimitive ( 'ERROR' , sourceInfo ) ;
}
}
2016-10-24 11:11:31 -07:00
private _reportError (
2016-10-21 11:41:14 -07:00
message : string , sourceSpan : ParseSourceSpan ,
level : ParseErrorLevel = ParseErrorLevel . FATAL ) {
2016-10-24 11:11:31 -07:00
this . _targetErrors . push ( new ParseError ( sourceSpan , message , level ) ) ;
2016-10-21 11:41:14 -07:00
}
private _reportExpressionParserErrors ( errors : ParserError [ ] , sourceSpan : ParseSourceSpan ) {
for ( const error of errors ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( error . message , sourceSpan ) ;
2016-10-21 11:41:14 -07:00
}
}
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 ) ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( ` The pipe ' ${ pipeName } ' could not be found ` , sourceSpan ) ;
2016-10-21 11:41:14 -07:00
}
} ) ;
}
}
/ * *
* @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 ) {
2016-10-24 11:11:31 -07:00
this . _reportError ( report . msg , sourceSpan , ParseErrorLevel . FATAL ) ;
2016-10-21 11:41:14 -07:00
}
}
}
export class PipeCollector extends RecursiveAstVisitor {
2016-10-24 11:11:31 -07:00
pipes = new Set < string > ( ) ;
2016-10-21 11:41:14 -07:00
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 ] == '@' ;
}