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
This commit is contained in:
ayazhafiz 2020-10-21 17:53:19 -05:00 committed by Joey Perrott
parent c83b2ad87f
commit 3241d922fc
1 changed files with 52 additions and 38 deletions

View File

@ -15,13 +15,11 @@ 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 {syntaxError} from '../util';
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-)|(@))(.*))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/;
const BIND_NAME_REGEXP = /^(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.*)$/;
// Group 1 = "bind-"
const KW_BIND_IDX = 1;
@ -37,12 +35,12 @@ const KW_BINDON_IDX = 5;
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 BINDING_DELIMS = {
BANANA_BOX: {start: '[(', end: ')]'},
PROPERTY: {start: '[', end: ']'},
EVENT: {start: '(', end: ')'},
};
const TEMPLATE_ATTR_PREFIX = '*';
@ -337,10 +335,8 @@ class HtmlAstToIvyAst implements html.Visitor {
}
const bindParts = name.match(BIND_NAME_REGEXP);
let hasBinding = false;
if (bindParts) {
hasBinding = true;
if (bindParts[KW_BIND_IDX] != null) {
const identifier = bindParts[IDENT_KW_IDX];
const keySpan = createKeySpan(srcSpan, bindParts[KW_BIND_IDX], identifier);
@ -380,36 +376,54 @@ class HtmlAstToIvyAst implements html.Visitor {
this.bindingParser.parseLiteralAttr(
name, value, srcSpan, absoluteOffset, attribute.valueSpan, matchableAttributes,
parsedProperties, keySpan);
} else if (bindParts[IDENT_BANANA_BOX_IDX]) {
const keySpan = createKeySpan(srcSpan, '[(', bindParts[IDENT_BANANA_BOX_IDX]);
this.bindingParser.parsePropertyBinding(
bindParts[IDENT_BANANA_BOX_IDX], value, false, srcSpan, absoluteOffset,
attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
this.parseAssignmentEvent(
bindParts[IDENT_BANANA_BOX_IDX], value, srcSpan, attribute.valueSpan,
matchableAttributes, boundEvents);
} else if (bindParts[IDENT_PROPERTY_IDX]) {
const keySpan = createKeySpan(srcSpan, '[', bindParts[IDENT_PROPERTY_IDX]);
this.bindingParser.parsePropertyBinding(
bindParts[IDENT_PROPERTY_IDX], value, false, srcSpan, absoluteOffset,
attribute.valueSpan, matchableAttributes, parsedProperties, keySpan);
} else if (bindParts[IDENT_EVENT_IDX]) {
const events: ParsedEvent[] = [];
this.bindingParser.parseEvent(
bindParts[IDENT_EVENT_IDX], value, srcSpan, attribute.valueSpan || srcSpan,
matchableAttributes, events);
addEvents(events, boundEvents);
}
} else {
const keySpan = createKeySpan(srcSpan, '' /* prefix */, name);
hasBinding = this.bindingParser.parsePropertyInterpolation(
name, value, srcSpan, 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);
if (delims.start === BINDING_DELIMS.BANANA_BOX.start) {
const keySpan = createKeySpan(srcSpan, delims.start, identifier);
this.bindingParser.parsePropertyBinding(
identifier, value, false, srcSpan, absoluteOffset, attribute.valueSpan,
matchableAttributes, parsedProperties, keySpan);
this.parseAssignmentEvent(
identifier, value, srcSpan, attribute.valueSpan, matchableAttributes, boundEvents);
} else if (delims.start === BINDING_DELIMS.PROPERTY.start) {
const keySpan = createKeySpan(srcSpan, delims.start, identifier);
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);
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;
}