/** * @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 {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, identifierName} from '../compile_metadata'; import {CompileReflector} from '../compile_reflector'; import {CompilerConfig} from '../config'; import {SchemaMetadata} from '../core'; import {AST, ASTWithSource, EmptyExpr, ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast'; import {Parser} from '../expression_parser/parser'; import {Identifiers, createTokenForExternalReference, createTokenForReference} from '../identifiers'; import * as html from '../ml_parser/ast'; import {HtmlParser, ParseTreeResult} from '../ml_parser/html_parser'; import {removeWhitespaces, replaceNgsp} from '../ml_parser/html_whitespaces'; import {expandNodes} from '../ml_parser/icu_ast_expander'; import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {isNgTemplate, splitNsName} from '../ml_parser/tags'; import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; 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 {Console, syntaxError} from '../util'; import {BindingParser} from './binding_parser'; import * as t from './template_ast'; import {PreparsedElementType, preparseElement} from './template_preparser'; const BIND_NAME_REGEXP = /^(?:(?:(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/; // Group 1 = "bind-" const KW_BIND_IDX = 1; // Group 2 = "let-" const KW_LET_IDX = 2; // Group 3 = "ref-/#" const KW_REF_IDX = 3; // Group 4 = "on-" const KW_ON_IDX = 4; // Group 5 = "bindon-" const KW_BINDON_IDX = 5; // Group 6 = "@" const KW_AT_IDX = 6; // Group 7 = the identifier after "bind-", "let-", "ref-/#", "on-", "bindon-" or "@" const IDENT_KW_IDX = 7; // Group 8 = identifier inside [()] const IDENT_BANANA_BOX_IDX = 8; // Group 9 = identifier inside [] const IDENT_PROPERTY_IDX = 9; // Group 10 = identifier inside () const IDENT_EVENT_IDX = 10; const TEMPLATE_ATTR_PREFIX = '*'; const CLASS_ATTR = 'class'; let _TEXT_CSS_SELECTOR !: CssSelector; function TEXT_CSS_SELECTOR(): CssSelector { if (!_TEXT_CSS_SELECTOR) { _TEXT_CSS_SELECTOR = CssSelector.parse('*')[0]; } return _TEXT_CSS_SELECTOR; } export class TemplateParseError extends ParseError { constructor(message: string, span: ParseSourceSpan, level: ParseErrorLevel) { super(span, message, level); } } export class TemplateParseResult { constructor( public templateAst?: t.TemplateAst[], public usedPipes?: CompilePipeSummary[], public errors?: ParseError[]) {} } export class TemplateParser { constructor( private _config: CompilerConfig, private _reflector: CompileReflector, private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry, private _htmlParser: HtmlParser, private _console: Console, public transforms: t.TemplateAstVisitor[]) {} public get expressionParser() { return this._exprParser; } parse( component: CompileDirectiveMetadata, template: string|ParseTreeResult, directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string, preserveWhitespaces: boolean): {template: t.TemplateAst[], pipes: CompilePipeSummary[]} { const result = this.tryParse( component, template, directives, pipes, schemas, templateUrl, preserveWhitespaces); const warnings = result.errors !.filter(error => error.level === ParseErrorLevel.WARNING); const errors = result.errors !.filter(error => error.level === ParseErrorLevel.ERROR); if (warnings.length > 0) { this._console.warn(`Template parse warnings:\n${warnings.join('\n')}`); } if (errors.length > 0) { const errorString = errors.join('\n'); throw syntaxError(`Template parse errors:\n${errorString}`, errors); } return {template: result.templateAst !, pipes: result.usedPipes !}; } tryParse( component: CompileDirectiveMetadata, template: string|ParseTreeResult, directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[], templateUrl: string, preserveWhitespaces: boolean): TemplateParseResult { let htmlParseResult = typeof template === 'string' ? this._htmlParser !.parse( template, templateUrl, true, this.getInterpolationConfig(component)) : template; if (!preserveWhitespaces) { htmlParseResult = removeWhitespaces(htmlParseResult); } return this.tryParseHtml( this.expandHtml(htmlParseResult), component, directives, pipes, schemas); } tryParseHtml( htmlAstWithErrors: ParseTreeResult, component: CompileDirectiveMetadata, directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[], schemas: SchemaMetadata[]): TemplateParseResult { let result: t.TemplateAst[]; const errors = htmlAstWithErrors.errors; const usedPipes: CompilePipeSummary[] = []; if (htmlAstWithErrors.rootNodes.length > 0) { const uniqDirectives = removeSummaryDuplicates(directives); const uniqPipes = removeSummaryDuplicates(pipes); const providerViewContext = new ProviderViewContext(this._reflector, component); let interpolationConfig: InterpolationConfig = undefined !; if (component.template && component.template.interpolation) { interpolationConfig = { start: component.template.interpolation[0], end: component.template.interpolation[1] }; } const bindingParser = new BindingParser( this._exprParser, interpolationConfig !, this._schemaRegistry, uniqPipes, errors); const parseVisitor = new TemplateParseVisitor( this._reflector, this._config, providerViewContext, uniqDirectives, bindingParser, this._schemaRegistry, schemas, errors); result = html.visitAll(parseVisitor, htmlAstWithErrors.rootNodes, EMPTY_ELEMENT_CONTEXT); errors.push(...providerViewContext.errors); usedPipes.push(...bindingParser.getUsedPipes()); } else { result = []; } this._assertNoReferenceDuplicationOnTemplate(result, errors); if (errors.length > 0) { return new TemplateParseResult(result, usedPipes, errors); } if (this.transforms) { this.transforms.forEach( (transform: t.TemplateAstVisitor) => { result = t.templateVisitAll(transform, result); }); } return new TemplateParseResult(result, usedPipes, errors); } expandHtml(htmlAstWithErrors: ParseTreeResult, forced: boolean = false): ParseTreeResult { const errors: ParseError[] = htmlAstWithErrors.errors; if (errors.length == 0 || forced) { // Transform ICU messages to angular directives const expandedHtmlAst = expandNodes(htmlAstWithErrors.rootNodes); errors.push(...expandedHtmlAst.errors); htmlAstWithErrors = new ParseTreeResult(expandedHtmlAst.nodes, errors); } return htmlAstWithErrors; } getInterpolationConfig(component: CompileDirectiveMetadata): InterpolationConfig|undefined { if (component.template) { return InterpolationConfig.fromArray(component.template.interpolation); } return undefined; } /** @internal */ _assertNoReferenceDuplicationOnTemplate(result: t.TemplateAst[], errors: TemplateParseError[]): void { const existingReferences: string[] = []; result.filter(element => !!(element).references) .forEach(element => (element).references.forEach((reference: t.ReferenceAst) => { const name = reference.name; if (existingReferences.indexOf(name) < 0) { existingReferences.push(name); } else { const error = new TemplateParseError( `Reference "#${name}" is defined several times`, reference.sourceSpan, ParseErrorLevel.ERROR); errors.push(error); } })); } } class TemplateParseVisitor implements html.Visitor { selectorMatcher = new SelectorMatcher(); directivesIndex = new Map(); ngContentCount = 0; contentQueryStartId: number; constructor( private reflector: CompileReflector, private config: CompilerConfig, public providerViewContext: ProviderViewContext, directives: CompileDirectiveSummary[], private _bindingParser: BindingParser, private _schemaRegistry: ElementSchemaRegistry, private _schemas: SchemaMetadata[], private _targetErrors: TemplateParseError[]) { // Note: queries start with id 1 so we can use the number in a Bloom filter! this.contentQueryStartId = providerViewContext.component.viewQueries.length + 1; directives.forEach((directive, index) => { const selector = CssSelector.parse(directive.selector !); this.selectorMatcher.addSelectables(selector, directive); this.directivesIndex.set(directive, index); }); } visitExpansion(expansion: html.Expansion, context: any): any { return null; } visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return null; } visitText(text: html.Text, parent: ElementContext): any { const ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR()) !; const valueNoNgsp = replaceNgsp(text.value); const expr = this._bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan !); return expr ? new t.BoundTextAst(expr, ngContentIndex, text.sourceSpan !) : new t.TextAst(valueNoNgsp, ngContentIndex, text.sourceSpan !); } visitAttribute(attribute: html.Attribute, context: any): any { return new t.AttrAst(attribute.name, attribute.value, attribute.sourceSpan); } visitComment(comment: html.Comment, context: any): any { return null; } visitElement(element: html.Element, parent: ElementContext): any { const queryStartIndex = this.contentQueryStartId; const elName = element.name; const preparsedElement = preparseElement(element); if (preparsedElement.type === PreparsedElementType.SCRIPT || preparsedElement.type === PreparsedElementType.STYLE) { // Skipping