angular-cn/packages/compiler/src/render3/r3_template_transform.ts

569 lines
23 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google LLC 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 {ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
import * as i18n from '../i18n/i18n_ast';
import * as html from '../ml_parser/ast';
import {replaceNgsp} from '../ml_parser/html_whitespaces';
import {isNgTemplate} from '../ml_parser/tags';
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {isStyleUrlResolvable} from '../style_url_resolver';
import {BindingParser} from '../template_parser/binding_parser';
import {PreparsedElementType, preparseElement} from '../template_parser/template_preparser';
import * as t from './r3_ast';
import {I18N_ICU_VAR_PREFIX, isI18nRootNode} from './view/i18n/util';
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;
const BINDING_DELIMS = {
BANANA_BOX: {start: '[(', end: ')]'},
PROPERTY: {start: '[', end: ']'},
EVENT: {start: '(', end: ')'},
};
const TEMPLATE_ATTR_PREFIX = '*';
// Result of the html AST to Ivy AST transformation
export interface Render3ParseResult {
nodes: t.Node[];
errors: ParseError[];
styles: string[];
styleUrls: string[];
feat(compiler): add dependency info and ng-content selectors to metadata (#35695) This commit augments the `FactoryDef` declaration of Angular decorated classes to contain information about the parameter decorators used in the constructor. If no constructor is present, or none of the parameters have any Angular decorators, then this will be represented using the `null` type. Otherwise, a tuple type is used where the entry at index `i` corresponds with parameter `i`. Each tuple entry can be one of two types: 1. If the associated parameter does not have any Angular decorators, the tuple entry will be the `null` type. 2. Otherwise, a type literal is used that may declare at least one of the following properties: - "attribute": if `@Attribute` is present. The injected attribute's name is used as string literal type, or the `unknown` type if the attribute name is not a string literal. - "self": if `@Self` is present, always of type `true`. - "skipSelf": if `@SkipSelf` is present, always of type `true`. - "host": if `@Host` is present, always of type `true`. - "optional": if `@Optional` is present, always of type `true`. A property is only present if the corresponding decorator is used. Note that the `@Inject` decorator is currently not included, as it's non-trivial to properly convert the token's value expression to a type that is valid in a declaration file. Additionally, the `ComponentDefWithMeta` declaration that is created for Angular components has been extended to include all selectors on `ng-content` elements within the component's template. This additional metadata is useful for tooling such as the Angular Language Service, as it provides the ability to offer suggestions for directives/components defined in libraries. At the moment, such tooling extracts the necessary information from the _metadata.json_ manifest file as generated by ngc, however this metadata representation is being replaced by the information emitted into the declaration files. Resolves FW-1870 PR Close #35695
2020-02-26 22:05:44 +01:00
ngContentSelectors: string[];
// Will be defined if `Render3ParseOptions['collectCommentNodes']` is true
commentNodes?: t.Comment[];
}
interface Render3ParseOptions {
collectCommentNodes: boolean;
}
export function htmlAstToRender3Ast(
htmlNodes: html.Node[], bindingParser: BindingParser,
options: Render3ParseOptions): Render3ParseResult {
const transformer = new HtmlAstToIvyAst(bindingParser, options);
const ivyNodes = html.visitAll(transformer, htmlNodes);
// Errors might originate in either the binding parser or the html to ivy transformer
const allErrors = bindingParser.errors.concat(transformer.errors);
const result: Render3ParseResult = {
nodes: ivyNodes,
errors: allErrors,
styleUrls: transformer.styleUrls,
styles: transformer.styles,
ngContentSelectors: transformer.ngContentSelectors
};
if (options.collectCommentNodes) {
result.commentNodes = transformer.commentNodes;
}
return result;
}
class HtmlAstToIvyAst implements html.Visitor {
errors: ParseError[] = [];
styles: string[] = [];
styleUrls: string[] = [];
feat(compiler): add dependency info and ng-content selectors to metadata (#35695) This commit augments the `FactoryDef` declaration of Angular decorated classes to contain information about the parameter decorators used in the constructor. If no constructor is present, or none of the parameters have any Angular decorators, then this will be represented using the `null` type. Otherwise, a tuple type is used where the entry at index `i` corresponds with parameter `i`. Each tuple entry can be one of two types: 1. If the associated parameter does not have any Angular decorators, the tuple entry will be the `null` type. 2. Otherwise, a type literal is used that may declare at least one of the following properties: - "attribute": if `@Attribute` is present. The injected attribute's name is used as string literal type, or the `unknown` type if the attribute name is not a string literal. - "self": if `@Self` is present, always of type `true`. - "skipSelf": if `@SkipSelf` is present, always of type `true`. - "host": if `@Host` is present, always of type `true`. - "optional": if `@Optional` is present, always of type `true`. A property is only present if the corresponding decorator is used. Note that the `@Inject` decorator is currently not included, as it's non-trivial to properly convert the token's value expression to a type that is valid in a declaration file. Additionally, the `ComponentDefWithMeta` declaration that is created for Angular components has been extended to include all selectors on `ng-content` elements within the component's template. This additional metadata is useful for tooling such as the Angular Language Service, as it provides the ability to offer suggestions for directives/components defined in libraries. At the moment, such tooling extracts the necessary information from the _metadata.json_ manifest file as generated by ngc, however this metadata representation is being replaced by the information emitted into the declaration files. Resolves FW-1870 PR Close #35695
2020-02-26 22:05:44 +01:00
ngContentSelectors: string[] = [];
// This array will be populated if `Render3ParseOptions['collectCommentNodes']` is true
commentNodes: t.Comment[] = [];
private inI18nBlock: boolean = false;
constructor(private bindingParser: BindingParser, private options: Render3ParseOptions) {}
// HTML visitor
visitElement(element: html.Element): t.Node|null {
const isI18nRootElement = isI18nRootNode(element.i18n);
if (isI18nRootElement) {
if (this.inI18nBlock) {
this.reportError(
'Cannot mark an element as translatable inside of a translatable section. Please remove the nested i18n marker.',
element.sourceSpan);
}
this.inI18nBlock = true;
}
const preparsedElement = preparseElement(element);
if (preparsedElement.type === PreparsedElementType.SCRIPT) {
return null;
} else if (preparsedElement.type === PreparsedElementType.STYLE) {
const contents = textContents(element);
if (contents !== null) {
this.styles.push(contents);
}
return null;
} else if (
preparsedElement.type === PreparsedElementType.STYLESHEET &&
isStyleUrlResolvable(preparsedElement.hrefAttr)) {
this.styleUrls.push(preparsedElement.hrefAttr);
return null;
}
// Whether the element is a `<ng-template>`
const isTemplateElement = isNgTemplate(element.name);
const parsedProperties: ParsedProperty[] = [];
const boundEvents: t.BoundEvent[] = [];
const variables: t.Variable[] = [];
const references: t.Reference[] = [];
const attributes: t.TextAttribute[] = [];
const i18nAttrsMeta: {[key: string]: i18n.I18nMeta} = {};
const templateParsedProperties: ParsedProperty[] = [];
const templateVariables: t.Variable[] = [];
// Whether the element has any *-attribute
let elementHasInlineTemplate = false;
for (const attribute of element.attrs) {
let hasBinding = false;
const normalizedName = normalizeAttributeName(attribute.name);
// `*attr` defines template bindings
let isTemplateBinding = false;
if (attribute.i18n) {
i18nAttrsMeta[attribute.name] = attribute.i18n;
}
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
// *-attributes
if (elementHasInlineTemplate) {
this.reportError(
`Can't have multiple template bindings on one element. Use only one attribute prefixed with *`,
attribute.sourceSpan);
}
isTemplateBinding = true;
elementHasInlineTemplate = true;
const templateValue = attribute.value;
const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
const parsedVariables: ParsedVariable[] = [];
const absoluteValueOffset = attribute.valueSpan ?
attribute.valueSpan.start.offset :
// If there is no value span the attribute does not have a value, like `attr` in
//`<div attr></div>`. In this case, point to one character beyond the last character of
// the attribute name.
attribute.sourceSpan.start.offset + attribute.name.length;
this.bindingParser.parseInlineTemplateBinding(
templateKey, templateValue, attribute.sourceSpan, absoluteValueOffset, [],
templateParsedProperties, parsedVariables, true /* isIvyAst */);
templateVariables.push(...parsedVariables.map(
v => new t.Variable(v.name, v.value, v.sourceSpan, v.keySpan, v.valueSpan)));
} else {
// Check for variables, events, property bindings, interpolation
hasBinding = this.parseAttribute(
isTemplateElement, attribute, [], parsedProperties, boundEvents, variables, references);
}
if (!hasBinding && !isTemplateBinding) {
// don't include the bindings as attributes as well in the AST
attributes.push(this.visitAttribute(attribute));
}
}
const children: t.Node[] =
html.visitAll(preparsedElement.nonBindable ? NON_BINDABLE_VISITOR : this, element.children);
let parsedElement: t.Node|undefined;
if (preparsedElement.type === PreparsedElementType.NG_CONTENT) {
// `<ng-content>`
if (element.children &&
!element.children.every(
(node: html.Node) => isEmptyTextNode(node) || isCommentNode(node))) {
this.reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
}
const selector = preparsedElement.selectAttr;
const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr));
parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n);
feat(compiler): add dependency info and ng-content selectors to metadata (#35695) This commit augments the `FactoryDef` declaration of Angular decorated classes to contain information about the parameter decorators used in the constructor. If no constructor is present, or none of the parameters have any Angular decorators, then this will be represented using the `null` type. Otherwise, a tuple type is used where the entry at index `i` corresponds with parameter `i`. Each tuple entry can be one of two types: 1. If the associated parameter does not have any Angular decorators, the tuple entry will be the `null` type. 2. Otherwise, a type literal is used that may declare at least one of the following properties: - "attribute": if `@Attribute` is present. The injected attribute's name is used as string literal type, or the `unknown` type if the attribute name is not a string literal. - "self": if `@Self` is present, always of type `true`. - "skipSelf": if `@SkipSelf` is present, always of type `true`. - "host": if `@Host` is present, always of type `true`. - "optional": if `@Optional` is present, always of type `true`. A property is only present if the corresponding decorator is used. Note that the `@Inject` decorator is currently not included, as it's non-trivial to properly convert the token's value expression to a type that is valid in a declaration file. Additionally, the `ComponentDefWithMeta` declaration that is created for Angular components has been extended to include all selectors on `ng-content` elements within the component's template. This additional metadata is useful for tooling such as the Angular Language Service, as it provides the ability to offer suggestions for directives/components defined in libraries. At the moment, such tooling extracts the necessary information from the _metadata.json_ manifest file as generated by ngc, however this metadata representation is being replaced by the information emitted into the declaration files. Resolves FW-1870 PR Close #35695
2020-02-26 22:05:44 +01:00
this.ngContentSelectors.push(selector);
} else if (isTemplateElement) {
// `<ng-template>`
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
parsedElement = new t.Template(
fix(ivy): match attribute selectors for content projection with inline-templates (#29041) The content projection mechanism is static, in that it only looks at the static template nodes before directives are matched and change detection is run. When you have a selector-based content projection the selection is based on nodes that are available in the template. For example: ``` <ng-content selector="[some-attr]"></ng-content> ``` would match ``` <div some-attr="..."></div> ``` If you have an inline-template in your projected nodes. For example: ``` <div *ngIf="..." some-attr="..."></div> ``` This gets pre-parsed and converted to a canonical form. For example: ``` <ng-template [ngIf]="..."> <div some-attr=".."></div> </ng-template> ``` Note that only structural attributes (e.g. `*ngIf`) stay with the `<ng-template>` node. The other attributes move to the contained element inside the template. When this happens in ivy, the ng-template content is removed from the component template function and is compiled into its own template function. But this means that the information about the attributes that were on the content are lost and the projection selection mechanism is unable to match the original `<div *ngIf="..." some-attr>`. This commit adds support for this in ivy. Attributes are separated into three groups (Bindings, Templates and "other"). For inline-templates the Bindings and "other" types are hoisted back from the contained node to the `template()` instruction, so that they can be used in content projection matching. PR Close #29041
2019-03-07 08:31:31 +00:00
element.name, attributes, attrs.bound, boundEvents, [/* no template attributes */],
children, references, variables, element.sourceSpan, element.startSourceSpan,
element.endSourceSpan, element.i18n);
} else {
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
parsedElement = new t.Element(
element.name, attributes, attrs.bound, boundEvents, children, references,
element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.i18n);
}
if (elementHasInlineTemplate) {
fix(ivy): match attribute selectors for content projection with inline-templates (#29041) The content projection mechanism is static, in that it only looks at the static template nodes before directives are matched and change detection is run. When you have a selector-based content projection the selection is based on nodes that are available in the template. For example: ``` <ng-content selector="[some-attr]"></ng-content> ``` would match ``` <div some-attr="..."></div> ``` If you have an inline-template in your projected nodes. For example: ``` <div *ngIf="..." some-attr="..."></div> ``` This gets pre-parsed and converted to a canonical form. For example: ``` <ng-template [ngIf]="..."> <div some-attr=".."></div> </ng-template> ``` Note that only structural attributes (e.g. `*ngIf`) stay with the `<ng-template>` node. The other attributes move to the contained element inside the template. When this happens in ivy, the ng-template content is removed from the component template function and is compiled into its own template function. But this means that the information about the attributes that were on the content are lost and the projection selection mechanism is unable to match the original `<div *ngIf="..." some-attr>`. This commit adds support for this in ivy. Attributes are separated into three groups (Bindings, Templates and "other"). For inline-templates the Bindings and "other" types are hoisted back from the contained node to the `template()` instruction, so that they can be used in content projection matching. PR Close #29041
2019-03-07 08:31:31 +00:00
// If this node is an inline-template (e.g. has *ngFor) then we need to create a template
// node that contains this node.
// Moreover, if the node is an element, then we need to hoist its attributes to the template
// node for matching against content projection selectors.
const attrs = this.extractAttributes('ng-template', templateParsedProperties, i18nAttrsMeta);
const templateAttrs: (t.TextAttribute|t.BoundAttribute)[] = [];
fix(ivy): match attribute selectors for content projection with inline-templates (#29041) The content projection mechanism is static, in that it only looks at the static template nodes before directives are matched and change detection is run. When you have a selector-based content projection the selection is based on nodes that are available in the template. For example: ``` <ng-content selector="[some-attr]"></ng-content> ``` would match ``` <div some-attr="..."></div> ``` If you have an inline-template in your projected nodes. For example: ``` <div *ngIf="..." some-attr="..."></div> ``` This gets pre-parsed and converted to a canonical form. For example: ``` <ng-template [ngIf]="..."> <div some-attr=".."></div> </ng-template> ``` Note that only structural attributes (e.g. `*ngIf`) stay with the `<ng-template>` node. The other attributes move to the contained element inside the template. When this happens in ivy, the ng-template content is removed from the component template function and is compiled into its own template function. But this means that the information about the attributes that were on the content are lost and the projection selection mechanism is unable to match the original `<div *ngIf="..." some-attr>`. This commit adds support for this in ivy. Attributes are separated into three groups (Bindings, Templates and "other"). For inline-templates the Bindings and "other" types are hoisted back from the contained node to the `template()` instruction, so that they can be used in content projection matching. PR Close #29041
2019-03-07 08:31:31 +00:00
attrs.literal.forEach(attr => templateAttrs.push(attr));
attrs.bound.forEach(attr => templateAttrs.push(attr));
const hoistedAttrs = parsedElement instanceof t.Element ?
{
attributes: parsedElement.attributes,
inputs: parsedElement.inputs,
outputs: parsedElement.outputs,
} :
{attributes: [], inputs: [], outputs: []};
// For <ng-template>s with structural directives on them, avoid passing i18n information to
// the wrapping template to prevent unnecessary i18n instructions from being generated. The
// necessary i18n meta information will be extracted from child elements.
const i18n = isTemplateElement && isI18nRootElement ? undefined : element.i18n;
// TODO(pk): test for this case
parsedElement = new t.Template(
(parsedElement as t.Element | t.Content).name, hoistedAttrs.attributes,
hoistedAttrs.inputs, hoistedAttrs.outputs, templateAttrs, [parsedElement],
[/* no references */], templateVariables, element.sourceSpan, element.startSourceSpan,
element.endSourceSpan, i18n);
}
if (isI18nRootElement) {
this.inI18nBlock = false;
}
return parsedElement;
}
visitAttribute(attribute: html.Attribute): t.TextAttribute {
return new t.TextAttribute(
attribute.name, attribute.value, attribute.sourceSpan, attribute.keySpan,
attribute.valueSpan, attribute.i18n);
}
visitText(text: html.Text): t.Node {
return this._visitTextWithInterpolation(text.value, text.sourceSpan, text.i18n);
}
visitExpansion(expansion: html.Expansion): t.Icu|null {
if (!expansion.i18n) {
// do not generate Icu in case it was created
// outside of i18n block in a template
return null;
}
if (!isI18nRootNode(expansion.i18n)) {
throw new Error(`Invalid type "${expansion.i18n.constructor}" for "i18n" property of ${
expansion.sourceSpan.toString()}. Expected a "Message"`);
}
const message = expansion.i18n;
const vars: {[name: string]: t.BoundText} = {};
const placeholders: {[name: string]: t.Text|t.BoundText} = {};
// extract VARs from ICUs - we process them separately while
// assembling resulting message via goog.getMsg function, since
// we need to pass them to top-level goog.getMsg call
Object.keys(message.placeholders).forEach(key => {
const value = message.placeholders[key];
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
// Currently when the `plural` or `select` keywords in an ICU contain trailing spaces (e.g.
// `{count, select , ...}`), these spaces are also included into the key names in ICU vars
// (e.g. "VAR_SELECT "). These trailing spaces are not desirable, since they will later be
// converted into `_` symbols while normalizing placeholder names, which might lead to
// mismatches at runtime (i.e. placeholder will not be replaced with the correct value).
const formattedKey = key.trim();
const ast = this.bindingParser.parseInterpolationExpression(value.text, value.sourceSpan);
vars[formattedKey] = new t.BoundText(ast, value.sourceSpan);
} else {
placeholders[key] = this._visitTextWithInterpolation(value.text, value.sourceSpan);
}
});
return new t.Icu(vars, placeholders, expansion.sourceSpan, message);
}
visitExpansionCase(expansionCase: html.ExpansionCase): null {
return null;
}
visitComment(comment: html.Comment): null {
if (this.options.collectCommentNodes) {
this.commentNodes.push(new t.Comment(comment.value || '', comment.sourceSpan));
}
return null;
}
// convert view engine `ParsedProperty` to a format suitable for IVY
private extractAttributes(
elementName: string, properties: ParsedProperty[],
i18nPropsMeta: {[key: string]: i18n.I18nMeta}):
{bound: t.BoundAttribute[], literal: t.TextAttribute[]} {
const bound: t.BoundAttribute[] = [];
const literal: t.TextAttribute[] = [];
properties.forEach(prop => {
const i18n = i18nPropsMeta[prop.name];
if (prop.isLiteral) {
literal.push(new t.TextAttribute(
prop.name, prop.expression.source || '', prop.sourceSpan, prop.keySpan, prop.valueSpan,
i18n));
} else {
// Note that validation is skipped and property mapping is disabled
// due to the fact that we need to make sure a given prop is not an
// input of a directive and directive matching happens at runtime.
const bep = this.bindingParser.createBoundElementProperty(
elementName, prop, /* skipValidation */ true, /* mapPropertyName */ false);
bound.push(t.BoundAttribute.fromBoundElementProperty(bep, i18n));
}
});
return {bound, literal};
}
private parseAttribute(
isTemplateElement: boolean, attribute: html.Attribute, matchableAttributes: string[][],
parsedProperties: ParsedProperty[], boundEvents: t.BoundEvent[], variables: t.Variable[],
references: t.Reference[]) {
const name = normalizeAttributeName(attribute.name);
const value = attribute.value;
const srcSpan = attribute.sourceSpan;
const absoluteOffset =
attribute.valueSpan ? attribute.valueSpan.start.offset : srcSpan.start.offset;
function createKeySpan(srcSpan: ParseSourceSpan, prefix: string, identifier: string) {
// We need to adjust the start location for the keySpan to account for the removed 'data-'
// prefix from `normalizeAttributeName`.
const normalizationAdjustment = attribute.name.length - name.length;
const keySpanStart = srcSpan.start.moveBy(prefix.length + normalizationAdjustment);
const keySpanEnd = keySpanStart.moveBy(identifier.length);
return new ParseSourceSpan(keySpanStart, keySpanEnd, keySpanStart, identifier);
}
const bindParts = name.match(BIND_NAME_REGEXP);
if (bindParts) {
if (bindParts[KW_BIND_IDX] != null) {
const identifier = bindParts[IDENT_KW_IDX];
const keySpan = createKeySpan(srcSpan, bindParts[KW_BIND_IDX], identifier);
this.bindingParser.parsePropertyBinding(
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
matchableAttributes, parsedProperties, keySpan);
} else if (bindParts[KW_LET_IDX]) {
if (isTemplateElement) {
const identifier = bindParts[IDENT_KW_IDX];
const keySpan = createKeySpan(srcSpan, bindParts[KW_LET_IDX], identifier);
this.parseVariable(identifier, value, srcSpan, keySpan, attribute.valueSpan, variables);
} else {
this.reportError(`"let-" is only supported on ng-template elements.`, srcSpan);
}
} else if (bindParts[KW_REF_IDX]) {
const identifier = bindParts[IDENT_KW_IDX];
const keySpan = createKeySpan(srcSpan, bindParts[KW_REF_IDX], identifier);
this.parseReference(identifier, value, srcSpan, keySpan, attribute.valueSpan, references);
} else if (bindParts[KW_ON_IDX]) {
const events: ParsedEvent[] = [];
const identifier = bindParts[IDENT_KW_IDX];
const keySpan = createKeySpan(srcSpan, bindParts[KW_ON_IDX], identifier);
this.bindingParser.parseEvent(
identifier, value, srcSpan, attribute.valueSpan || srcSpan, matchableAttributes, events,
keySpan);
addEvents(events, boundEvents);
} else if (bindParts[KW_BINDON_IDX]) {
const identifier = bindParts[IDENT_KW_IDX];
const keySpan = createKeySpan(srcSpan, bindParts[KW_BINDON_IDX], identifier);
this.bindingParser.parsePropertyBinding(
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
matchableAttributes, parsedProperties, keySpan);
this.parseAssignmentEvent(
identifier, value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents,
keySpan);
} else if (bindParts[KW_AT_IDX]) {
const keySpan = createKeySpan(srcSpan, '', name);
this.bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes,
parsedProperties, keySpan);
}
return true;
}
// We didn't see a kw-prefixed property binding, but we have not yet checked
// for the []/()/[()] syntax.
let delims: {start: string, end: string}|null = null;
if (name.startsWith(BINDING_DELIMS.BANANA_BOX.start)) {
delims = BINDING_DELIMS.BANANA_BOX;
} else if (name.startsWith(BINDING_DELIMS.PROPERTY.start)) {
delims = BINDING_DELIMS.PROPERTY;
} else if (name.startsWith(BINDING_DELIMS.EVENT.start)) {
delims = BINDING_DELIMS.EVENT;
}
if (delims !== null &&
// NOTE: older versions of the parser would match a start/end delimited
// binding iff the property name was terminated by the ending delimiter
// and the identifier in the binding was non-empty.
// TODO(ayazhafiz): update this to handle malformed bindings.
name.endsWith(delims.end) && name.length > delims.start.length + delims.end.length) {
const identifier = name.substring(delims.start.length, name.length - delims.end.length);
const keySpan = createKeySpan(srcSpan, delims.start, identifier);
if (delims.start === BINDING_DELIMS.BANANA_BOX.start) {
this.bindingParser.parsePropertyBinding(
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
matchableAttributes, parsedProperties, keySpan);
this.parseAssignmentEvent(
identifier, value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents,
keySpan);
} else if (delims.start === BINDING_DELIMS.PROPERTY.start) {
this.bindingParser.parsePropertyBinding(
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
matchableAttributes, parsedProperties, keySpan);
} else {
const events: ParsedEvent[] = [];
this.bindingParser.parseEvent(
identifier, value, srcSpan, attribute.valueSpan || srcSpan, matchableAttributes, events,
keySpan);
addEvents(events, boundEvents);
}
return true;
}
// No explicit binding found.
const keySpan = createKeySpan(srcSpan, '' /* prefix */, name);
const hasBinding = this.bindingParser.parsePropertyInterpolation(
name, value, srcSpan, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
return hasBinding;
}
private _visitTextWithInterpolation(
value: string, sourceSpan: ParseSourceSpan, i18n?: i18n.I18nMeta): t.Text|t.BoundText {
const valueNoNgsp = replaceNgsp(value);
const expr = this.bindingParser.parseInterpolation(valueNoNgsp, sourceSpan);
return expr ? new t.BoundText(expr, sourceSpan, i18n) : new t.Text(valueNoNgsp, sourceSpan);
}
private parseVariable(
identifier: string, value: string, sourceSpan: ParseSourceSpan, keySpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, variables: t.Variable[]) {
if (identifier.indexOf('-') > -1) {
this.reportError(`"-" is not allowed in variable names`, sourceSpan);
} else if (identifier.length === 0) {
this.reportError(`Variable does not have a name`, sourceSpan);
}
variables.push(new t.Variable(identifier, value, sourceSpan, keySpan, valueSpan));
}
private parseReference(
identifier: string, value: string, sourceSpan: ParseSourceSpan, keySpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, references: t.Reference[]) {
if (identifier.indexOf('-') > -1) {
this.reportError(`"-" is not allowed in reference names`, sourceSpan);
} else if (identifier.length === 0) {
this.reportError(`Reference does not have a name`, sourceSpan);
} else if (references.some(reference => reference.name === identifier)) {
this.reportError(`Reference "#${identifier}" is defined more than once`, sourceSpan);
}
references.push(new t.Reference(identifier, value, sourceSpan, keySpan, valueSpan));
}
private parseAssignmentEvent(
name: string, expression: string, sourceSpan: ParseSourceSpan,
valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
boundEvents: t.BoundEvent[], keySpan: ParseSourceSpan) {
const events: ParsedEvent[] = [];
this.bindingParser.parseEvent(
`${name}Change`, `${expression}=$event`, sourceSpan, valueSpan || sourceSpan,
targetMatchableAttrs, events, keySpan);
addEvents(events, boundEvents);
}
private reportError(
message: string, sourceSpan: ParseSourceSpan,
level: ParseErrorLevel = ParseErrorLevel.ERROR) {
this.errors.push(new ParseError(sourceSpan, message, level));
}
}
class NonBindableVisitor implements html.Visitor {
visitElement(ast: html.Element): t.Element|null {
const preparsedElement = preparseElement(ast);
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
preparsedElement.type === PreparsedElementType.STYLE ||
preparsedElement.type === PreparsedElementType.STYLESHEET) {
// Skipping <script> for security reasons
// Skipping <style> and stylesheets as we already processed them
// in the StyleCompiler
return null;
}
const children: t.Node[] = html.visitAll(this, ast.children, null);
return new t.Element(
ast.name, html.visitAll(this, ast.attrs) as t.TextAttribute[],
/* inputs */[], /* outputs */[], children,  /* references */[], ast.sourceSpan,
ast.startSourceSpan, ast.endSourceSpan);
}
visitComment(comment: html.Comment): any {
return null;
}
visitAttribute(attribute: html.Attribute): t.TextAttribute {
return new t.TextAttribute(
attribute.name, attribute.value, attribute.sourceSpan, attribute.keySpan,
attribute.valueSpan, attribute.i18n);
}
visitText(text: html.Text): t.Text {
return new t.Text(text.value, text.sourceSpan);
}
visitExpansion(expansion: html.Expansion): any {
return null;
}
visitExpansionCase(expansionCase: html.ExpansionCase): any {
return null;
}
}
const NON_BINDABLE_VISITOR = new NonBindableVisitor();
function normalizeAttributeName(attrName: string): string {
return /^data-/i.test(attrName) ? attrName.substring(5) : attrName;
}
function addEvents(events: ParsedEvent[], boundEvents: t.BoundEvent[]) {
boundEvents.push(...events.map(e => t.BoundEvent.fromParsedEvent(e)));
}
function isEmptyTextNode(node: html.Node): boolean {
return node instanceof html.Text && node.value.trim().length == 0;
}
function isCommentNode(node: html.Node): boolean {
return node instanceof html.Comment;
}
function textContents(node: html.Element): string|null {
if (node.children.length !== 1 || !(node.children[0] instanceof html.Text)) {
return null;
} else {
return (node.children[0] as html.Text).value;
}
}