2018-04-18 16:23:49 -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
|
|
|
|
|
*/
|
|
|
|
|
|
2018-08-02 11:32:04 -07:00
|
|
|
|
import {ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
|
2018-10-18 10:08:51 -07:00
|
|
|
|
import * as i18n from '../i18n/i18n_ast';
|
2018-04-18 16:23:49 -07:00
|
|
|
|
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';
|
2018-04-20 11:28:34 -07:00
|
|
|
|
import {BindingParser} from '../template_parser/binding_parser';
|
2018-04-18 16:23:49 -07:00
|
|
|
|
import {PreparsedElementType, preparseElement} from '../template_parser/template_preparser';
|
2018-04-27 14:39:07 -07:00
|
|
|
|
import {syntaxError} from '../util';
|
2018-08-02 11:32:04 -07:00
|
|
|
|
|
2018-04-18 16:23:49 -07:00
|
|
|
|
import * as t from './r3_ast';
|
2019-09-11 14:00:59 -07:00
|
|
|
|
import {I18N_ICU_VAR_PREFIX, isI18nRootNode} from './view/i18n/util';
|
2018-04-27 14:39:07 -07:00
|
|
|
|
|
2018-04-18 16:23:49 -07:00
|
|
|
|
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 = '*';
|
|
|
|
|
|
2018-04-27 14:39:07 -07:00
|
|
|
|
// Result of the html AST to Ivy AST transformation
|
2019-02-26 14:48:42 -08:00
|
|
|
|
export interface Render3ParseResult {
|
|
|
|
|
nodes: t.Node[];
|
|
|
|
|
errors: ParseError[];
|
|
|
|
|
styles: string[];
|
|
|
|
|
styleUrls: string[];
|
|
|
|
|
}
|
2018-04-27 14:39:07 -07:00
|
|
|
|
|
|
|
|
|
export function htmlAstToRender3Ast(
|
|
|
|
|
htmlNodes: html.Node[], bindingParser: BindingParser): Render3ParseResult {
|
|
|
|
|
const transformer = new HtmlAstToIvyAst(bindingParser);
|
|
|
|
|
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 errors: ParseError[] = allErrors.filter(e => e.level === ParseErrorLevel.ERROR);
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
const errorString = errors.join('\n');
|
|
|
|
|
throw syntaxError(`Template parse errors:\n${errorString}`, errors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
nodes: ivyNodes,
|
|
|
|
|
errors: allErrors,
|
2019-02-26 14:48:42 -08:00
|
|
|
|
styleUrls: transformer.styleUrls,
|
|
|
|
|
styles: transformer.styles,
|
2018-04-27 14:39:07 -07:00
|
|
|
|
};
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
2018-04-27 14:39:07 -07:00
|
|
|
|
class HtmlAstToIvyAst implements html.Visitor {
|
|
|
|
|
errors: ParseError[] = [];
|
2019-02-26 14:48:42 -08:00
|
|
|
|
styles: string[] = [];
|
|
|
|
|
styleUrls: string[] = [];
|
2019-08-03 12:24:48 -07:00
|
|
|
|
private inI18nBlock: boolean = false;
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
constructor(private bindingParser: BindingParser) {}
|
|
|
|
|
|
|
|
|
|
// HTML visitor
|
|
|
|
|
visitElement(element: html.Element): t.Node|null {
|
2019-08-03 12:24:48 -07:00
|
|
|
|
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;
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
const preparsedElement = preparseElement(element);
|
2019-02-26 14:48:42 -08:00
|
|
|
|
if (preparsedElement.type === PreparsedElementType.SCRIPT) {
|
2018-04-18 16:23:49 -07:00
|
|
|
|
return null;
|
2019-02-26 14:48:42 -08:00
|
|
|
|
} 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 &&
|
2018-04-18 16:23:49 -07:00
|
|
|
|
isStyleUrlResolvable(preparsedElement.hrefAttr)) {
|
2019-02-26 14:48:42 -08:00
|
|
|
|
this.styleUrls.push(preparsedElement.hrefAttr);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Whether the element is a `<ng-template>`
|
|
|
|
|
const isTemplateElement = isNgTemplate(element.name);
|
|
|
|
|
|
2018-04-20 11:28:34 -07:00
|
|
|
|
const parsedProperties: ParsedProperty[] = [];
|
2018-04-18 16:23:49 -07:00
|
|
|
|
const boundEvents: t.BoundEvent[] = [];
|
|
|
|
|
const variables: t.Variable[] = [];
|
|
|
|
|
const references: t.Reference[] = [];
|
|
|
|
|
const attributes: t.TextAttribute[] = [];
|
2019-10-22 15:05:44 +01:00
|
|
|
|
const i18nAttrsMeta: {[key: string]: i18n.I18nMeta} = {};
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
2018-04-20 11:28:34 -07:00
|
|
|
|
const templateParsedProperties: ParsedProperty[] = [];
|
2018-04-18 16:23:49 -07:00
|
|
|
|
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;
|
|
|
|
|
|
2018-10-18 10:08:51 -07:00
|
|
|
|
if (attribute.i18n) {
|
|
|
|
|
i18nAttrsMeta[attribute.name] = attribute.i18n;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-18 16:23:49 -07:00
|
|
|
|
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
2018-08-02 11:32:04 -07:00
|
|
|
|
// *-attributes
|
2018-04-18 16:23:49 -07:00
|
|
|
|
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;
|
2018-04-19 17:23:27 -07:00
|
|
|
|
const templateValue = attribute.value;
|
|
|
|
|
const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
2018-04-24 14:22:55 -07:00
|
|
|
|
const parsedVariables: ParsedVariable[] = [];
|
2019-07-16 12:18:32 -07:00
|
|
|
|
const absoluteOffset = attribute.valueSpan ? attribute.valueSpan.start.offset :
|
|
|
|
|
attribute.sourceSpan.start.offset;
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseInlineTemplateBinding(
|
2019-07-16 12:18:32 -07:00
|
|
|
|
templateKey, templateValue, attribute.sourceSpan, absoluteOffset, [],
|
|
|
|
|
templateParsedProperties, parsedVariables);
|
2018-04-24 14:22:55 -07:00
|
|
|
|
templateVariables.push(
|
|
|
|
|
...parsedVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan)));
|
2018-04-18 16:23:49 -07:00
|
|
|
|
} else {
|
|
|
|
|
// Check for variables, events, property bindings, interpolation
|
|
|
|
|
hasBinding = this.parseAttribute(
|
2018-08-02 11:32:04 -07:00
|
|
|
|
isTemplateElement, attribute, [], parsedProperties, boundEvents, variables, references);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasBinding && !isTemplateBinding) {
|
|
|
|
|
// don't include the bindings as attributes as well in the AST
|
|
|
|
|
attributes.push(this.visitAttribute(attribute) as t.TextAttribute);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>`
|
2019-02-19 18:28:00 -08:00
|
|
|
|
if (element.children &&
|
|
|
|
|
!element.children.every(
|
|
|
|
|
(node: html.Node) => isEmptyTextNode(node) || isCommentNode(node))) {
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.reportError(`<ng-content> element cannot have content.`, element.sourceSpan);
|
|
|
|
|
}
|
|
|
|
|
const selector = preparsedElement.selectAttr;
|
2018-11-30 15:01:37 -08:00
|
|
|
|
const attrs: t.TextAttribute[] = element.attrs.map(attr => this.visitAttribute(attr));
|
|
|
|
|
parsedElement = new t.Content(selector, attrs, element.sourceSpan, element.i18n);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
} else if (isTemplateElement) {
|
|
|
|
|
// `<ng-template>`
|
2018-10-18 10:08:51 -07:00
|
|
|
|
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
2018-08-02 11:32:04 -07:00
|
|
|
|
|
2018-04-18 16:23:49 -07:00
|
|
|
|
parsedElement = new t.Template(
|
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);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
} else {
|
2018-10-18 10:08:51 -07:00
|
|
|
|
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
parsedElement = new t.Element(
|
2018-08-02 11:32:04 -07:00
|
|
|
|
element.name, attributes, attrs.bound, boundEvents, children, references,
|
2018-10-18 10:08:51 -07:00
|
|
|
|
element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.i18n);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (elementHasInlineTemplate) {
|
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.
|
2018-10-18 10:08:51 -07:00
|
|
|
|
const attrs = this.extractAttributes('ng-template', templateParsedProperties, i18nAttrsMeta);
|
2019-03-07 08:31:31 +00:00
|
|
|
|
const templateAttrs: (t.TextAttribute | t.BoundAttribute)[] = [];
|
|
|
|
|
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: []};
|
2019-09-11 14:00:59 -07:00
|
|
|
|
|
|
|
|
|
// 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.
|
2019-08-03 12:24:48 -07:00
|
|
|
|
const i18n = isTemplateElement && isI18nRootElement ? undefined : element.i18n;
|
2019-09-11 14:00:59 -07:00
|
|
|
|
|
2018-08-20 15:20:12 +02:00
|
|
|
|
// TODO(pk): test for this case
|
2018-04-18 16:23:49 -07:00
|
|
|
|
parsedElement = new t.Template(
|
2019-03-07 08:31:31 +00:00
|
|
|
|
(parsedElement as t.Element).name, hoistedAttrs.attributes, hoistedAttrs.inputs,
|
|
|
|
|
hoistedAttrs.outputs, templateAttrs, [parsedElement], [/* no references */],
|
2018-12-12 15:23:12 -08:00
|
|
|
|
templateVariables, element.sourceSpan, element.startSourceSpan, element.endSourceSpan,
|
2019-09-11 14:00:59 -07:00
|
|
|
|
i18n);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
2019-08-03 12:24:48 -07:00
|
|
|
|
if (isI18nRootElement) {
|
|
|
|
|
this.inI18nBlock = false;
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
return parsedElement;
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-02 11:32:04 -07:00
|
|
|
|
visitAttribute(attribute: html.Attribute): t.TextAttribute {
|
2018-04-18 16:23:49 -07:00
|
|
|
|
return new t.TextAttribute(
|
2018-10-18 10:08:51 -07:00
|
|
|
|
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan, attribute.i18n);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visitText(text: html.Text): t.Node {
|
2018-10-18 10:08:51 -07:00
|
|
|
|
return this._visitTextWithInterpolation(text.value, text.sourceSpan, text.i18n);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-18 10:08:51 -07:00
|
|
|
|
visitExpansion(expansion: html.Expansion): t.Icu|null {
|
2019-10-22 15:05:44 +01:00
|
|
|
|
if (!expansion.i18n) {
|
|
|
|
|
// do not generate Icu in case it was created
|
|
|
|
|
// outside of i18n block in a template
|
2018-10-18 10:08:51 -07:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2019-10-22 15:05:44 +01:00
|
|
|
|
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;
|
2018-10-18 10:08:51 -07:00
|
|
|
|
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
|
2019-10-22 15:05:44 +01:00
|
|
|
|
Object.keys(message.placeholders).forEach(key => {
|
|
|
|
|
const value = message.placeholders[key];
|
2018-10-18 10:08:51 -07:00
|
|
|
|
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
2018-11-29 16:21:16 -08:00
|
|
|
|
const config = this.bindingParser.interpolationConfig;
|
|
|
|
|
// ICU expression is a plain string, not wrapped into start
|
|
|
|
|
// and end tags, so we wrap it before passing to binding parser
|
|
|
|
|
const wrapped = `${config.start}${value}${config.end}`;
|
|
|
|
|
vars[key] = this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText;
|
2018-10-18 10:08:51 -07:00
|
|
|
|
} else {
|
|
|
|
|
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
|
|
|
|
|
}
|
|
|
|
|
});
|
2019-10-22 15:05:44 +01:00
|
|
|
|
return new t.Icu(vars, placeholders, expansion.sourceSpan, message);
|
2018-10-18 10:08:51 -07:00
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
visitExpansionCase(expansionCase: html.ExpansionCase): null { return null; }
|
|
|
|
|
|
2018-10-18 10:08:51 -07:00
|
|
|
|
visitComment(comment: html.Comment): null { return null; }
|
|
|
|
|
|
2018-08-02 11:32:04 -07:00
|
|
|
|
// convert view engine `ParsedProperty` to a format suitable for IVY
|
2018-10-18 10:08:51 -07:00
|
|
|
|
private extractAttributes(
|
2019-10-22 15:05:44 +01:00
|
|
|
|
elementName: string, properties: ParsedProperty[],
|
|
|
|
|
i18nPropsMeta: {[key: string]: i18n.I18nMeta}):
|
2018-08-02 11:32:04 -07:00
|
|
|
|
{bound: t.BoundAttribute[], literal: t.TextAttribute[]} {
|
|
|
|
|
const bound: t.BoundAttribute[] = [];
|
|
|
|
|
const literal: t.TextAttribute[] = [];
|
|
|
|
|
|
|
|
|
|
properties.forEach(prop => {
|
2018-10-18 10:08:51 -07:00
|
|
|
|
const i18n = i18nPropsMeta[prop.name];
|
2018-08-02 11:32:04 -07:00
|
|
|
|
if (prop.isLiteral) {
|
2018-10-18 10:08:51 -07:00
|
|
|
|
literal.push(new t.TextAttribute(
|
|
|
|
|
prop.name, prop.expression.source || '', prop.sourceSpan, undefined, i18n));
|
2018-08-02 11:32:04 -07:00
|
|
|
|
} else {
|
2019-02-15 21:55:07 +01:00
|
|
|
|
// 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.
|
2019-01-10 13:34:39 -08:00
|
|
|
|
const bep = this.bindingParser.createBoundElementProperty(
|
2019-02-15 21:55:07 +01:00
|
|
|
|
elementName, prop, /* skipValidation */ true, /* mapPropertyName */ false);
|
2018-10-18 10:08:51 -07:00
|
|
|
|
bound.push(t.BoundAttribute.fromBoundElementProperty(bep, i18n));
|
2018-08-02 11:32:04 -07:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {bound, literal};
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private parseAttribute(
|
|
|
|
|
isTemplateElement: boolean, attribute: html.Attribute, matchableAttributes: string[][],
|
2018-04-20 11:28:34 -07:00
|
|
|
|
parsedProperties: ParsedProperty[], boundEvents: t.BoundEvent[], variables: t.Variable[],
|
2018-04-18 16:23:49 -07:00
|
|
|
|
references: t.Reference[]) {
|
|
|
|
|
const name = normalizeAttributeName(attribute.name);
|
|
|
|
|
const value = attribute.value;
|
|
|
|
|
const srcSpan = attribute.sourceSpan;
|
2019-07-16 12:18:32 -07:00
|
|
|
|
const absoluteOffset =
|
|
|
|
|
attribute.valueSpan ? attribute.valueSpan.start.offset : srcSpan.start.offset;
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
const bindParts = name.match(BIND_NAME_REGEXP);
|
|
|
|
|
let hasBinding = false;
|
|
|
|
|
|
|
|
|
|
if (bindParts) {
|
|
|
|
|
hasBinding = true;
|
|
|
|
|
if (bindParts[KW_BIND_IDX] != null) {
|
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
2019-05-04 22:41:17 +02:00
|
|
|
|
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan,
|
|
|
|
|
matchableAttributes, parsedProperties);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
} else if (bindParts[KW_LET_IDX]) {
|
|
|
|
|
if (isTemplateElement) {
|
|
|
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
2019-05-04 22:41:17 +02:00
|
|
|
|
this.parseVariable(identifier, value, srcSpan, attribute.valueSpan, variables);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
} else {
|
|
|
|
|
this.reportError(`"let-" is only supported on ng-template elements.`, srcSpan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (bindParts[KW_REF_IDX]) {
|
|
|
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
2019-05-04 22:41:17 +02:00
|
|
|
|
this.parseReference(identifier, value, srcSpan, attribute.valueSpan, references);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
} else if (bindParts[KW_ON_IDX]) {
|
2018-04-20 11:28:34 -07:00
|
|
|
|
const events: ParsedEvent[] = [];
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseEvent(
|
2019-02-08 22:10:20 +00:00
|
|
|
|
bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan || srcSpan,
|
|
|
|
|
matchableAttributes, events);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
addEvents(events, boundEvents);
|
|
|
|
|
} else if (bindParts[KW_BINDON_IDX]) {
|
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
2019-05-04 22:41:17 +02:00
|
|
|
|
bindParts[IDENT_KW_IDX], value, false, srcSpan, absoluteOffset, attribute.valueSpan,
|
|
|
|
|
matchableAttributes, parsedProperties);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.parseAssignmentEvent(
|
2019-02-08 22:10:20 +00:00
|
|
|
|
bindParts[IDENT_KW_IDX], value, srcSpan, attribute.valueSpan, matchableAttributes,
|
|
|
|
|
boundEvents);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
} else if (bindParts[KW_AT_IDX]) {
|
|
|
|
|
this.bindingParser.parseLiteralAttr(
|
2019-05-04 22:41:17 +02:00
|
|
|
|
name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes,
|
|
|
|
|
parsedProperties);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
} else if (bindParts[IDENT_BANANA_BOX_IDX]) {
|
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
2019-07-16 12:18:32 -07:00
|
|
|
|
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset,
|
2019-05-04 22:41:17 +02:00
|
|
|
|
attribute.valueSpan, matchableAttributes, parsedProperties);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.parseAssignmentEvent(
|
2019-02-08 22:10:20 +00:00
|
|
|
|
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan,
|
|
|
|
|
matchableAttributes, boundEvents);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
} else if (bindParts[IDENT_PROPERTY_IDX]) {
|
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
2019-07-16 12:18:32 -07:00
|
|
|
|
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset,
|
2019-05-04 22:41:17 +02:00
|
|
|
|
attribute.valueSpan, matchableAttributes, parsedProperties);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
} else if (bindParts[IDENT_EVENT_IDX]) {
|
2018-04-20 11:28:34 -07:00
|
|
|
|
const events: ParsedEvent[] = [];
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseEvent(
|
2019-02-08 22:10:20 +00:00
|
|
|
|
bindParts[IDENT_EVENT_IDX], value, srcSpan, attribute.valueSpan || srcSpan,
|
|
|
|
|
matchableAttributes, events);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
addEvents(events, boundEvents);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
hasBinding = this.bindingParser.parsePropertyInterpolation(
|
2019-05-04 22:41:17 +02:00
|
|
|
|
name, value, srcSpan, attribute.valueSpan, matchableAttributes, parsedProperties);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hasBinding;
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-22 15:05:44 +01:00
|
|
|
|
private _visitTextWithInterpolation(
|
|
|
|
|
value: string, sourceSpan: ParseSourceSpan, i18n?: i18n.I18nMeta): t.Text|t.BoundText {
|
2018-10-18 10:08:51 -07:00
|
|
|
|
const valueNoNgsp = replaceNgsp(value);
|
|
|
|
|
const expr = this.bindingParser.parseInterpolation(valueNoNgsp, sourceSpan);
|
|
|
|
|
return expr ? new t.BoundText(expr, sourceSpan, i18n) : new t.Text(valueNoNgsp, sourceSpan);
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-18 16:23:49 -07:00
|
|
|
|
private parseVariable(
|
2019-05-04 22:41:17 +02:00
|
|
|
|
identifier: string, value: string, sourceSpan: ParseSourceSpan,
|
|
|
|
|
valueSpan: ParseSourceSpan|undefined, variables: t.Variable[]) {
|
2018-04-18 16:23:49 -07:00
|
|
|
|
if (identifier.indexOf('-') > -1) {
|
|
|
|
|
this.reportError(`"-" is not allowed in variable names`, sourceSpan);
|
|
|
|
|
}
|
2019-05-04 22:41:17 +02:00
|
|
|
|
variables.push(new t.Variable(identifier, value, sourceSpan, valueSpan));
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private parseReference(
|
2019-05-04 22:41:17 +02:00
|
|
|
|
identifier: string, value: string, sourceSpan: ParseSourceSpan,
|
|
|
|
|
valueSpan: ParseSourceSpan|undefined, references: t.Reference[]) {
|
2018-04-18 16:23:49 -07:00
|
|
|
|
if (identifier.indexOf('-') > -1) {
|
|
|
|
|
this.reportError(`"-" is not allowed in reference names`, sourceSpan);
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-04 22:41:17 +02:00
|
|
|
|
references.push(new t.Reference(identifier, value, sourceSpan, valueSpan));
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private parseAssignmentEvent(
|
|
|
|
|
name: string, expression: string, sourceSpan: ParseSourceSpan,
|
2019-02-08 22:10:20 +00:00
|
|
|
|
valueSpan: ParseSourceSpan|undefined, targetMatchableAttrs: string[][],
|
|
|
|
|
boundEvents: t.BoundEvent[]) {
|
2018-04-20 11:28:34 -07:00
|
|
|
|
const events: ParsedEvent[] = [];
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseEvent(
|
2019-02-08 22:10:20 +00:00
|
|
|
|
`${name}Change`, `${expression}=$event`, sourceSpan, valueSpan || sourceSpan,
|
|
|
|
|
targetMatchableAttrs, events);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
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 {
|
2018-10-18 10:08:51 -07:00
|
|
|
|
return new t.TextAttribute(
|
|
|
|
|
attribute.name, attribute.value, attribute.sourceSpan, undefined, attribute.i18n);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-20 11:28:34 -07:00
|
|
|
|
function addEvents(events: ParsedEvent[], boundEvents: t.BoundEvent[]) {
|
|
|
|
|
boundEvents.push(...events.map(e => t.BoundEvent.fromParsedEvent(e)));
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isEmptyTextNode(node: html.Node): boolean {
|
|
|
|
|
return node instanceof html.Text && node.value.trim().length == 0;
|
|
|
|
|
}
|
2019-02-19 18:28:00 -08:00
|
|
|
|
|
|
|
|
|
function isCommentNode(node: html.Node): boolean {
|
|
|
|
|
return node instanceof html.Comment;
|
|
|
|
|
}
|
2019-02-26 14:48:42 -08:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2019-07-16 12:18:32 -07:00
|
|
|
|
}
|