2018-04-18 16:23:49 -07:00
|
|
|
|
/**
|
|
|
|
|
* @license
|
2020-05-19 12:08:49 -07:00
|
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2018-04-18 16:23:49 -07:00
|
|
|
|
*
|
|
|
|
|
* 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-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
|
|
|
|
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
const BIND_NAME_REGEXP = /^(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.*)$/;
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
// 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;
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
|
|
|
|
|
const BINDING_DELIMS = {
|
|
|
|
|
BANANA_BOX: {start: '[(', end: ')]'},
|
|
|
|
|
PROPERTY: {start: '[', end: ']'},
|
|
|
|
|
EVENT: {start: '(', end: ')'},
|
|
|
|
|
};
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
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[];
|
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[];
|
2021-03-17 23:18:30 +04:00
|
|
|
|
// Will be defined if `Render3ParseOptions['collectCommentNodes']` is true
|
|
|
|
|
commentNodes?: t.Comment[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Render3ParseOptions {
|
|
|
|
|
collectCommentNodes: boolean;
|
2019-02-26 14:48:42 -08:00
|
|
|
|
}
|
2018-04-27 14:39:07 -07:00
|
|
|
|
|
|
|
|
|
export function htmlAstToRender3Ast(
|
2021-03-17 23:18:30 +04:00
|
|
|
|
htmlNodes: html.Node[], bindingParser: BindingParser,
|
|
|
|
|
options: Render3ParseOptions): Render3ParseResult {
|
|
|
|
|
const transformer = new HtmlAstToIvyAst(bindingParser, options);
|
2018-04-27 14:39:07 -07:00
|
|
|
|
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);
|
|
|
|
|
|
2021-03-17 23:18:30 +04:00
|
|
|
|
const result: Render3ParseResult = {
|
2018-04-27 14:39:07 -07:00
|
|
|
|
nodes: ivyNodes,
|
|
|
|
|
errors: allErrors,
|
2019-02-26 14:48:42 -08:00
|
|
|
|
styleUrls: transformer.styleUrls,
|
|
|
|
|
styles: transformer.styles,
|
2021-03-17 23:18:30 +04:00
|
|
|
|
ngContentSelectors: transformer.ngContentSelectors
|
2018-04-27 14:39:07 -07:00
|
|
|
|
};
|
2021-03-17 23:18:30 +04:00
|
|
|
|
if (options.collectCommentNodes) {
|
|
|
|
|
result.commentNodes = transformer.commentNodes;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
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[] = [];
|
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[] = [];
|
2021-03-17 23:18:30 +04:00
|
|
|
|
// This array will be populated if `Render3ParseOptions['collectCommentNodes']` is true
|
|
|
|
|
commentNodes: t.Comment[] = [];
|
2019-08-03 12:24:48 -07:00
|
|
|
|
private inI18nBlock: boolean = false;
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
2021-03-17 23:18:30 +04:00
|
|
|
|
constructor(private bindingParser: BindingParser, private options: Render3ParseOptions) {}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
// 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-23 15:51:42 -07:00
|
|
|
|
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;
|
|
|
|
|
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseInlineTemplateBinding(
|
2019-07-23 15:51:42 -07:00
|
|
|
|
templateKey, templateValue, attribute.sourceSpan, absoluteValueOffset, [],
|
2020-09-28 21:23:34 -07:00
|
|
|
|
templateParsedProperties, parsedVariables, true /* isIvyAst */);
|
2020-03-11 16:46:08 -07:00
|
|
|
|
templateVariables.push(...parsedVariables.map(
|
2020-09-23 16:28:36 -07:00
|
|
|
|
v => new t.Variable(v.name, v.value, v.sourceSpan, v.keySpan, v.valueSpan)));
|
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
|
2020-11-09 10:25:25 -08:00
|
|
|
|
attributes.push(this.visitAttribute(attribute));
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
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);
|
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);
|
2020-04-08 10:14:18 -07:00
|
|
|
|
const templateAttrs: (t.TextAttribute|t.BoundAttribute)[] = [];
|
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: []};
|
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(
|
2020-07-15 07:03:04 +02:00
|
|
|
|
(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);
|
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(
|
2020-11-09 10:25:25 -08:00
|
|
|
|
attribute.name, attribute.value, attribute.sourceSpan, attribute.keySpan,
|
|
|
|
|
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)) {
|
2020-04-08 10:14:18 -07:00
|
|
|
|
throw new Error(`Invalid type "${expansion.i18n.constructor}" for "i18n" property of ${
|
|
|
|
|
expansion.sourceSpan.toString()}. Expected a "Message"`);
|
2019-10-22 15:05:44 +01:00
|
|
|
|
}
|
|
|
|
|
const message = expansion.i18n;
|
2018-10-18 10:08:51 -07:00
|
|
|
|
const vars: {[name: string]: t.BoundText} = {};
|
2020-04-08 10:14:18 -07:00
|
|
|
|
const placeholders: {[name: string]: t.Text|t.BoundText} = {};
|
2018-10-18 10:08:51 -07:00
|
|
|
|
// 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)) {
|
2020-06-29 17:50:21 -07:00
|
|
|
|
// 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();
|
|
|
|
|
|
2020-10-01 00:17:21 +02:00
|
|
|
|
const ast = this.bindingParser.parseInterpolationExpression(value.text, value.sourceSpan);
|
|
|
|
|
|
|
|
|
|
vars[formattedKey] = new t.BoundText(ast, value.sourceSpan);
|
2018-10-18 10:08:51 -07:00
|
|
|
|
} else {
|
2020-10-01 00:17:21 +02:00
|
|
|
|
placeholders[key] = this._visitTextWithInterpolation(value.text, value.sourceSpan);
|
2018-10-18 10:08:51 -07:00
|
|
|
|
}
|
|
|
|
|
});
|
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
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
|
visitExpansionCase(expansionCase: html.ExpansionCase): null {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
|
visitComment(comment: html.Comment): null {
|
2021-03-17 23:18:30 +04:00
|
|
|
|
if (this.options.collectCommentNodes) {
|
|
|
|
|
this.commentNodes.push(new t.Comment(comment.value || '', comment.sourceSpan));
|
|
|
|
|
}
|
2020-04-08 10:14:18 -07:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-10-18 10:08:51 -07:00
|
|
|
|
|
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(
|
2020-11-09 10:25:25 -08:00
|
|
|
|
prop.name, prop.expression.source || '', prop.sourceSpan, prop.keySpan, prop.valueSpan,
|
|
|
|
|
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
|
|
|
|
|
2020-09-17 13:01:32 -07:00
|
|
|
|
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);
|
2020-10-28 21:35:06 +00:00
|
|
|
|
return new ParseSourceSpan(keySpanStart, keySpanEnd, keySpanStart, identifier);
|
2020-09-17 13:01:32 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-04-18 16:23:49 -07:00
|
|
|
|
const bindParts = name.match(BIND_NAME_REGEXP);
|
|
|
|
|
|
|
|
|
|
if (bindParts) {
|
|
|
|
|
if (bindParts[KW_BIND_IDX] != null) {
|
2020-09-17 13:01:32 -07:00
|
|
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
|
|
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_BIND_IDX], identifier);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
2020-09-17 13:01:32 -07:00
|
|
|
|
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
|
|
|
|
|
matchableAttributes, parsedProperties, keySpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
} else if (bindParts[KW_LET_IDX]) {
|
|
|
|
|
if (isTemplateElement) {
|
|
|
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
2020-09-23 16:28:36 -07:00
|
|
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_LET_IDX], identifier);
|
|
|
|
|
this.parseVariable(identifier, value, srcSpan, keySpan, 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];
|
2020-11-09 13:35:48 -08:00
|
|
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_REF_IDX], identifier);
|
|
|
|
|
this.parseReference(identifier, value, srcSpan, keySpan, 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[] = [];
|
2020-09-17 13:01:32 -07:00
|
|
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
2020-11-09 09:16:19 -08:00
|
|
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_ON_IDX], identifier);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseEvent(
|
2020-11-09 09:16:19 -08:00
|
|
|
|
identifier, value, srcSpan, attribute.valueSpan || srcSpan, matchableAttributes, events,
|
|
|
|
|
keySpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
addEvents(events, boundEvents);
|
|
|
|
|
} else if (bindParts[KW_BINDON_IDX]) {
|
2020-09-17 13:01:32 -07:00
|
|
|
|
const identifier = bindParts[IDENT_KW_IDX];
|
|
|
|
|
const keySpan = createKeySpan(srcSpan, bindParts[KW_BINDON_IDX], identifier);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
2020-09-17 13:01:32 -07:00
|
|
|
|
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
|
|
|
|
|
matchableAttributes, parsedProperties, keySpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.parseAssignmentEvent(
|
2020-11-09 09:16:19 -08:00
|
|
|
|
identifier, value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents,
|
|
|
|
|
keySpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
} else if (bindParts[KW_AT_IDX]) {
|
2020-09-17 13:01:32 -07:00
|
|
|
|
const keySpan = createKeySpan(srcSpan, '', name);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseLiteralAttr(
|
2019-05-04 22:41:17 +02:00
|
|
|
|
name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes,
|
2020-09-17 13:01:32 -07:00
|
|
|
|
parsedProperties, keySpan);
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
// 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);
|
2020-11-09 09:16:19 -08:00
|
|
|
|
const keySpan = createKeySpan(srcSpan, delims.start, identifier);
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
if (delims.start === BINDING_DELIMS.BANANA_BOX.start) {
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
|
|
|
|
|
matchableAttributes, parsedProperties, keySpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.parseAssignmentEvent(
|
2020-11-09 09:16:19 -08:00
|
|
|
|
identifier, value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents,
|
|
|
|
|
keySpan);
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
} else if (delims.start === BINDING_DELIMS.PROPERTY.start) {
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parsePropertyBinding(
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
|
|
|
|
|
matchableAttributes, parsedProperties, keySpan);
|
|
|
|
|
} else {
|
2018-04-20 11:28:34 -07:00
|
|
|
|
const events: ParsedEvent[] = [];
|
2018-04-18 16:23:49 -07:00
|
|
|
|
this.bindingParser.parseEvent(
|
2020-11-09 09:16:19 -08:00
|
|
|
|
identifier, value, srcSpan, attribute.valueSpan || srcSpan, matchableAttributes, events,
|
|
|
|
|
keySpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
addEvents(events, boundEvents);
|
|
|
|
|
}
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
|
|
|
|
|
return true;
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
refactor(compiler): parse bindings "by hand" rather than via regex (#39375)
To support recovery of malformed binding property names like `([a)`,
`[a`, or `()`, the binding parser needs to be more permissive w.r.t. the
kinds of bindings it can detect. This is difficult to do maintainably
with a regex, but is trivial with a "hand-rolled" string parser. This
commit refactors render3's binding attribute parsing to use this method
for multi-delimited bindings (namely via the `()`, `[]`, and `[()]`)
syntax, making the way recovery of malformed bindings in a future patch.
Note that we can keep using a regex for prefix-only binding syntax
(e.g. `bind-`, `ref-`) because validation of the binding is complete
once we have matched the prefix, and the only thing left to do is check
that the binding identifier is non-empty, which is trivial.
Part of #38596
PR Close #39375
2020-10-21 17:53:19 -05:00
|
|
|
|
// No explicit binding found.
|
|
|
|
|
const keySpan = createKeySpan(srcSpan, '' /* prefix */, name);
|
|
|
|
|
const hasBinding = this.bindingParser.parsePropertyInterpolation(
|
|
|
|
|
name, value, srcSpan, attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
|
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(
|
2020-09-23 16:28:36 -07:00
|
|
|
|
identifier: string, value: string, sourceSpan: ParseSourceSpan, keySpan: ParseSourceSpan,
|
2019-05-04 22:41:17 +02:00
|
|
|
|
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-12-28 18:02:09 -06:00
|
|
|
|
} else if (identifier.length === 0) {
|
|
|
|
|
this.reportError(`Variable does not have a name`, sourceSpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
2019-12-28 18:02:09 -06:00
|
|
|
|
|
2020-09-23 16:28:36 -07:00
|
|
|
|
variables.push(new t.Variable(identifier, value, sourceSpan, keySpan, valueSpan));
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private parseReference(
|
2020-11-09 13:35:48 -08:00
|
|
|
|
identifier: string, value: string, sourceSpan: ParseSourceSpan, keySpan: ParseSourceSpan,
|
2019-05-04 22:41:17 +02:00
|
|
|
|
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-12-28 18:02:09 -06:00
|
|
|
|
} else if (identifier.length === 0) {
|
|
|
|
|
this.reportError(`Reference does not have a name`, sourceSpan);
|
2021-01-23 22:10:12 +01:00
|
|
|
|
} else if (references.some(reference => reference.name === identifier)) {
|
|
|
|
|
this.reportError(`Reference "#${identifier}" is defined more than once`, sourceSpan);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-09 13:35:48 -08:00
|
|
|
|
references.push(new t.Reference(identifier, value, sourceSpan, keySpan, 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[][],
|
2020-11-09 09:16:19 -08:00
|
|
|
|
boundEvents: t.BoundEvent[], keySpan: ParseSourceSpan) {
|
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,
|
2020-11-09 09:16:19 -08:00
|
|
|
|
targetMatchableAttrs, events, keySpan);
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
|
visitComment(comment: html.Comment): any {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
|
|
|
|
visitAttribute(attribute: html.Attribute): t.TextAttribute {
|
2018-10-18 10:08:51 -07:00
|
|
|
|
return new t.TextAttribute(
|
2020-11-09 10:25:25 -08:00
|
|
|
|
attribute.name, attribute.value, attribute.sourceSpan, attribute.keySpan,
|
|
|
|
|
attribute.valueSpan, attribute.i18n);
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
|
visitText(text: html.Text): t.Text {
|
|
|
|
|
return new t.Text(text.value, text.sourceSpan);
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
|
visitExpansion(expansion: html.Expansion): any {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
|
2020-04-08 10:14:18 -07:00
|
|
|
|
visitExpansionCase(expansionCase: html.ExpansionCase): any {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-04-18 16:23:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|