refactor(compiler): refactor template binding parsing (#23460)

A long time ago Angular used to support both those attribute notations:
- `*attr='binding'`
- `template=`attr: binding`

Because the last notation has been dropped we can refactor the binding parsing.
Source maps will benefit from that as no `attr:` prefix is added artificialy any
more.

PR Close #23460
This commit is contained in:
Victor Berchet 2018-04-19 17:23:27 -07:00
parent ca776c59dd
commit 4662878a1f
7 changed files with 557 additions and 573 deletions

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CommonModule, NgForOf} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, Directive} from '@angular/core'; import {Component} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';

View File

@ -215,7 +215,7 @@ export class ASTWithSource extends AST {
export class TemplateBinding { export class TemplateBinding {
constructor( constructor(
public span: ParseSpan, public key: string, public keyIsVar: boolean, public name: string, public span: ParseSpan, public key: string, public keyIsVar: boolean, public name: string,
public expression: ASTWithSource) {} public expression: ASTWithSource|null) {}
} }
export interface AstVisitor { export interface AstVisitor {

View File

@ -98,19 +98,11 @@ export class Parser {
return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location); return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location);
} }
parseTemplateBindings(prefixToken: string|null, input: string, location: any): parseTemplateBindings(tplKey: string, tplValue: string, location: any):
TemplateBindingParseResult { TemplateBindingParseResult {
const tokens = this._lexer.tokenize(input); const tokens = this._lexer.tokenize(tplValue);
if (prefixToken) { return new _ParseAST(tplValue, location, tokens, tplValue.length, false, this.errors, 0)
// Prefix the tokens with the tokens from prefixToken but have them take no space (0 index). .parseTemplateBindings(tplKey);
const prefixTokens = this._lexer.tokenize(prefixToken).map(t => {
t.index = 0;
return t;
});
tokens.unshift(...prefixTokens);
}
return new _ParseAST(input, location, tokens, input.length, false, this.errors, 0)
.parseTemplateBindings();
} }
parseInterpolation( parseInterpolation(
@ -686,48 +678,49 @@ export class _ParseAST {
return result.toString(); return result.toString();
} }
parseTemplateBindings(): TemplateBindingParseResult { // Parses the AST for `<some-tag *tplKey=AST>`
parseTemplateBindings(tplKey: string): TemplateBindingParseResult {
let firstBinding = true;
const bindings: TemplateBinding[] = []; const bindings: TemplateBinding[] = [];
let prefix: string = null !;
const warnings: string[] = []; const warnings: string[] = [];
while (this.index < this.tokens.length) { do {
const start = this.inputIndex; const start = this.inputIndex;
let keyIsVar: boolean = this.peekKeywordLet(); let rawKey: string;
if (keyIsVar) { let key: string;
this.advance(); let isVar: boolean = false;
} if (firstBinding) {
let rawKey = this.expectTemplateBindingKey(); rawKey = key = tplKey;
let key = rawKey; firstBinding = false;
if (!keyIsVar) {
if (prefix == null) {
prefix = key;
} else { } else {
key = prefix + key[0].toUpperCase() + key.substring(1); isVar = this.peekKeywordLet();
} if (isVar) this.advance()
} rawKey = this.expectTemplateBindingKey();
key = isVar ? rawKey : tplKey + rawKey[0].toUpperCase() + rawKey.substring(1);
this.optionalCharacter(chars.$COLON); this.optionalCharacter(chars.$COLON);
}
let name: string = null !; let name: string = null !;
let expression: ASTWithSource = null !; let expression: ASTWithSource|null = null;
if (keyIsVar) { if (isVar) {
if (this.optionalOperator('=')) { if (this.optionalOperator('=')) {
name = this.expectTemplateBindingKey(); name = this.expectTemplateBindingKey();
} else { } else {
name = '\$implicit'; name = '\$implicit';
} }
} else if (this.peekKeywordAs()) { } else if (this.peekKeywordAs()) {
const letStart = this.inputIndex;
this.advance(); // consume `as` this.advance(); // consume `as`
name = rawKey; name = rawKey;
key = this.expectTemplateBindingKey(); // read local var name key = this.expectTemplateBindingKey(); // read local var name
keyIsVar = true; isVar = true;
} else if (this.next !== EOF && !this.peekKeywordLet()) { } else if (this.next !== EOF && !this.peekKeywordLet()) {
const start = this.inputIndex; const start = this.inputIndex;
const ast = this.parsePipe(); const ast = this.parsePipe();
const source = this.input.substring(start - this.offset, this.inputIndex - this.offset); const source = this.input.substring(start - this.offset, this.inputIndex - this.offset);
expression = new ASTWithSource(ast, source, this.location, this.errors); expression = new ASTWithSource(ast, source, this.location, this.errors);
} }
bindings.push(new TemplateBinding(this.span(start), key, keyIsVar, name, expression));
if (this.peekKeywordAs() && !keyIsVar) { bindings.push(new TemplateBinding(this.span(start), key, isVar, name, expression));
if (this.peekKeywordAs() && !isVar) {
const letStart = this.inputIndex; const letStart = this.inputIndex;
this.advance(); // consume `as` this.advance(); // consume `as`
const letName = this.expectTemplateBindingKey(); // read local var name const letName = this.expectTemplateBindingKey(); // read local var name
@ -736,7 +729,8 @@ export class _ParseAST {
if (!this.optionalCharacter(chars.$SEMICOLON)) { if (!this.optionalCharacter(chars.$SEMICOLON)) {
this.optionalCharacter(chars.$COMMA); this.optionalCharacter(chars.$COMMA);
} }
} } while (this.index < this.tokens.length)
return new TemplateBindingParseResult(bindings, warnings, this.errors); return new TemplateBindingParseResult(bindings, warnings, this.errors);
} }

View File

@ -107,16 +107,16 @@ export class HtmlToTemplateTransform implements html.Visitor {
} }
isTemplateBinding = true; isTemplateBinding = true;
elementHasInlineTemplate = true; elementHasInlineTemplate = true;
const templateBindingsSource = attribute.value; const templateValue = attribute.value;
const prefixToken = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length) + ':'; const templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
const oldVariables: VariableAst[] = []; const oldVariables: VariableAst[] = [];
inlineTemplateSourceSpan = attribute.valueSpan || attribute.sourceSpan; inlineTemplateSourceSpan = attribute.valueSpan || attribute.sourceSpan;
this.bindingParser.parseInlineTemplateBinding( this.bindingParser.parseInlineTemplateBinding(
prefixToken !, templateBindingsSource !, attribute.sourceSpan, templateKey, templateValue, attribute.sourceSpan, templateMatchableAttributes,
templateMatchableAttributes, templateBoundProperties, oldVariables); templateBoundProperties, oldVariables);
templateVariables.push( templateVariables.push(
...oldVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan))); ...oldVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan)));

View File

@ -130,11 +130,12 @@ export class BindingParser {
} }
} }
// Parse an inline template binding. ie `<tag *prefixToken="<value>">` // Parse an inline template binding. ie `<tag *tplKey="<tplValue>">`
parseInlineTemplateBinding( parseInlineTemplateBinding(
prefixToken: string, value: string, sourceSpan: ParseSourceSpan, tplKey: string, tplValue: string, sourceSpan: ParseSourceSpan,
targetMatchableAttrs: string[][], targetProps: BoundProperty[], targetVars: VariableAst[]) { targetMatchableAttrs: string[][], targetProps: BoundProperty[], targetVars: VariableAst[]) {
const bindings = this._parseTemplateBindings(prefixToken, value, sourceSpan); const bindings = this._parseTemplateBindings(tplKey, tplValue, sourceSpan);
for (let i = 0; i < bindings.length; i++) { for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]; const binding = bindings[i];
if (binding.keyIsVar) { if (binding.keyIsVar) {
@ -149,12 +150,12 @@ export class BindingParser {
} }
} }
private _parseTemplateBindings(prefixToken: string, value: string, sourceSpan: ParseSourceSpan): private _parseTemplateBindings(tplKey: string, tplValue: string, sourceSpan: ParseSourceSpan):
TemplateBinding[] { TemplateBinding[] {
const sourceInfo = sourceSpan.start.toString(); const sourceInfo = sourceSpan.start.toString();
try { try {
const bindingsResult = this._exprParser.parseTemplateBindings(prefixToken, value, sourceInfo); const bindingsResult = this._exprParser.parseTemplateBindings(tplKey, tplValue, sourceInfo);
this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan); this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
bindingsResult.templateBindings.forEach((binding) => { bindingsResult.templateBindings.forEach((binding) => {
if (binding.expression) { if (binding.expression) {

View File

@ -289,16 +289,16 @@ class TemplateParseVisitor implements html.Visitor {
isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events, isTemplateElement, attr, matchableAttrs, elementOrDirectiveProps, events,
elementOrDirectiveRefs, elementVars); elementOrDirectiveRefs, elementVars);
let templateBindingsSource: string|undefined; let templateValue: string|undefined;
let prefixToken: string|undefined; let templateKey: string|undefined;
const normalizedName = this._normalizeAttributeName(attr.name); const normalizedName = this._normalizeAttributeName(attr.name);
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) { if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
templateBindingsSource = attr.value; templateValue = attr.value;
prefixToken = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length) + ':'; templateKey = normalizedName.substring(TEMPLATE_ATTR_PREFIX.length);
} }
const hasTemplateBinding = templateBindingsSource != null; const hasTemplateBinding = templateValue != null;
if (hasTemplateBinding) { if (hasTemplateBinding) {
if (hasInlineTemplates) { if (hasInlineTemplates) {
this._reportError( this._reportError(
@ -307,7 +307,7 @@ class TemplateParseVisitor implements html.Visitor {
} }
hasInlineTemplates = true; hasInlineTemplates = true;
this._bindingParser.parseInlineTemplateBinding( this._bindingParser.parseInlineTemplateBinding(
prefixToken !, templateBindingsSource !, attr.sourceSpan, templateMatchableAttrs, templateKey !, templateValue !, attr.sourceSpan, templateMatchableAttrs,
templateElementOrDirectiveProps, templateElementVars); templateElementOrDirectiveProps, templateElementVars);
} }

View File

@ -15,80 +15,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
import {unparse} from './unparser'; import {unparse} from './unparser';
import {validate} from './validator'; import {validate} from './validator';
(function() { describe('parser', () => {
function createParser() { return new Parser(new Lexer()); }
function parseAction(text: string, location: any = null): ASTWithSource {
return createParser().parseAction(text, location);
}
function parseBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseBinding(text, location);
}
function parseTemplateBindingsResult(
text: string, location: any = null, prefix?: string): TemplateBindingParseResult {
return createParser().parseTemplateBindings(prefix || null, text, location);
}
function parseTemplateBindings(
text: string, location: any = null, prefix?: string): TemplateBinding[] {
return parseTemplateBindingsResult(text, location, prefix).templateBindings;
}
function parseInterpolation(text: string, location: any = null): ASTWithSource|null {
return createParser().parseInterpolation(text, location);
}
function splitInterpolation(text: string, location: any = null): SplitInterpolation|null {
return createParser().splitInterpolation(text, location);
}
function parseSimpleBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseSimpleBinding(text, location);
}
function checkInterpolation(exp: string, expected?: string) {
const ast = parseInterpolation(exp) !;
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkBinding(exp: string, expected?: string) {
const ast = parseBinding(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkAction(exp: string, expected?: string) {
const ast = parseAction(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function expectError(ast: {errors: ParserError[]}, message: string) {
for (const error of ast.errors) {
if (error.message.indexOf(message) >= 0) {
return;
}
}
const errMsgs = ast.errors.map(err => err.message).join('\n');
throw Error(
`Expected an error containing "${message}" to be reported, but got the errors:\n` +
errMsgs);
}
function expectActionError(text: string, message: string) {
expectError(validate(parseAction(text)), message);
}
function expectBindingError(text: string, message: string) {
expectError(validate(parseBinding(text)), message);
}
describe('parser', () => {
describe('parseAction', () => { describe('parseAction', () => {
it('should parse numbers', () => { checkAction('1'); }); it('should parse numbers', () => { checkAction('1'); });
@ -321,9 +248,7 @@ import {validate} from './validator';
describe('parseTemplateBindings', () => { describe('parseTemplateBindings', () => {
function keys(templateBindings: any[]) { function keys(templateBindings: any[]) { return templateBindings.map(binding => binding.key); }
return templateBindings.map(binding => binding.key);
}
function keyValues(templateBindings: any[]) { function keyValues(templateBindings: any[]) {
return templateBindings.map(binding => { return templateBindings.map(binding => {
@ -345,131 +270,126 @@ import {validate} from './validator';
binding => binding.expression != null ? binding.expression.source : null); binding => binding.expression != null ? binding.expression.source : null);
} }
it('should parse an empty string', () => { expect(parseTemplateBindings('')).toEqual([]); }); it('should parse a key without a value',
() => { expect(keys(parseTemplateBindings('a', ''))).toEqual(['a']); });
it('should parse a string without a value', it('should allow string including dashes as keys', () => {
() => { expect(keys(parseTemplateBindings('a'))).toEqual(['a']); }); let bindings = parseTemplateBindings('a', 'b');
it('should only allow identifier, string, or keyword including dashes as keys', () => {
let bindings = parseTemplateBindings('a:\'b\'');
expect(keys(bindings)).toEqual(['a']); expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('\'a\':\'b\''); bindings = parseTemplateBindings('a-b', 'c');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('"a":\'b\'');
expect(keys(bindings)).toEqual(['a']);
bindings = parseTemplateBindings('a-b:\'c\'');
expect(keys(bindings)).toEqual(['a-b']); expect(keys(bindings)).toEqual(['a-b']);
expectError(parseTemplateBindingsResult('(:0'), 'expected identifier, keyword, or string');
expectError(
parseTemplateBindingsResult('1234:0'), 'expected identifier, keyword, or string');
}); });
it('should detect expressions as value', () => { it('should detect expressions as value', () => {
let bindings = parseTemplateBindings('a:b'); let bindings = parseTemplateBindings('a', 'b');
expect(exprSources(bindings)).toEqual(['b']); expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings('a:1+1'); bindings = parseTemplateBindings('a', '1+1');
expect(exprSources(bindings)).toEqual(['1+1']); expect(exprSources(bindings)).toEqual(['1+1']);
}); });
it('should detect names as value', () => { it('should detect names as value', () => {
const bindings = parseTemplateBindings('a:let b'); const bindings = parseTemplateBindings('a', 'let b');
expect(keyValues(bindings)).toEqual(['a', 'let b=\$implicit']); expect(keyValues(bindings)).toEqual(['a', 'let b=$implicit']);
}); });
it('should allow space and colon as separators', () => { it('should allow space and colon as separators', () => {
let bindings = parseTemplateBindings('a:b'); let bindings = parseTemplateBindings('a', 'b');
expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']);
bindings = parseTemplateBindings('a b');
expect(keys(bindings)).toEqual(['a']); expect(keys(bindings)).toEqual(['a']);
expect(exprSources(bindings)).toEqual(['b']); expect(exprSources(bindings)).toEqual(['b']);
}); });
it('should allow multiple pairs', () => { it('should allow multiple pairs', () => {
const bindings = parseTemplateBindings('a 1 b 2'); const bindings = parseTemplateBindings('a', '1 b 2');
expect(keys(bindings)).toEqual(['a', 'aB']); expect(keys(bindings)).toEqual(['a', 'aB']);
expect(exprSources(bindings)).toEqual(['1 ', '2']); expect(exprSources(bindings)).toEqual(['1 ', '2']);
}); });
it('should store the sources in the result', () => { it('should store the sources in the result', () => {
const bindings = parseTemplateBindings('a 1,b 2'); const bindings = parseTemplateBindings('a', '1,b 2');
expect(bindings[0].expression.source).toEqual('1'); expect(bindings[0].expression !.source).toEqual('1');
expect(bindings[1].expression.source).toEqual('2'); expect(bindings[1].expression !.source).toEqual('2');
}); });
it('should store the passed-in location', () => { it('should store the passed-in location', () => {
const bindings = parseTemplateBindings('a 1,b 2', 'location'); const bindings = parseTemplateBindings('a', '1,b 2', 'location');
expect(bindings[0].expression.location).toEqual('location'); expect(bindings[0].expression !.location).toEqual('location');
}); });
it('should support let notation', () => { it('should support let notation', () => {
let bindings = parseTemplateBindings('let i'); let bindings = parseTemplateBindings('key', 'let i');
expect(keyValues(bindings)).toEqual(['let i=\$implicit']); expect(keyValues(bindings)).toEqual(['key', 'let i=$implicit']);
bindings = parseTemplateBindings('let i'); bindings = parseTemplateBindings('key', 'let a; let b');
expect(keyValues(bindings)).toEqual(['let i=\$implicit']);
bindings = parseTemplateBindings('let a; let b');
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings('let a; let b;');
expect(keyValues(bindings)).toEqual(['let a=\$implicit', 'let b=\$implicit']);
bindings = parseTemplateBindings('let i-a = k-a');
expect(keyValues(bindings)).toEqual(['let i-a=k-a']);
bindings = parseTemplateBindings('keyword let item; let i = k');
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings('keyword: let item; let i = k');
expect(keyValues(bindings)).toEqual(['keyword', 'let item=\$implicit', 'let i=k']);
bindings = parseTemplateBindings('directive: let item in expr; let a = b', 'location');
expect(keyValues(bindings)).toEqual([ expect(keyValues(bindings)).toEqual([
'directive', 'let item=\$implicit', 'directiveIn=expr in location', 'let a=b' 'key',
'let a=$implicit',
'let b=$implicit',
]);
bindings = parseTemplateBindings('key', 'let a; let b;');
expect(keyValues(bindings)).toEqual([
'key',
'let a=$implicit',
'let b=$implicit',
]);
bindings = parseTemplateBindings('key', 'let i-a = k-a');
expect(keyValues(bindings)).toEqual([
'key',
'let i-a=k-a',
]);
bindings = parseTemplateBindings('key', 'let item; let i = k');
expect(keyValues(bindings)).toEqual([
'key',
'let item=$implicit',
'let i=k',
]);
bindings = parseTemplateBindings('directive', 'let item in expr; let a = b', 'location');
expect(keyValues(bindings)).toEqual([
'directive',
'let item=$implicit',
'directiveIn=expr in location',
'let a=b',
]); ]);
}); });
it('should support as notation', () => { it('should support as notation', () => {
let bindings = parseTemplateBindings('ngIf exp as local', 'location'); let bindings = parseTemplateBindings('ngIf', 'exp as local', 'location');
expect(keyValues(bindings)).toEqual(['ngIf=exp in location', 'let local=ngIf']); expect(keyValues(bindings)).toEqual(['ngIf=exp in location', 'let local=ngIf']);
bindings = parseTemplateBindings('ngFor let item of items as iter; index as i', 'L'); bindings = parseTemplateBindings('ngFor', 'let item of items as iter; index as i', 'L');
expect(keyValues(bindings)).toEqual([ expect(keyValues(bindings)).toEqual([
'ngFor', 'let item=$implicit', 'ngForOf=items in L', 'let iter=ngForOf', 'let i=index' 'ngFor', 'let item=$implicit', 'ngForOf=items in L', 'let iter=ngForOf', 'let i=index'
]); ]);
}); });
it('should parse pipes', () => { it('should parse pipes', () => {
const bindings = parseTemplateBindings('key value|pipe'); const bindings = parseTemplateBindings('key', 'value|pipe');
const ast = bindings[0].expression.ast; const ast = bindings[0].expression !.ast;
expect(ast).toBeAnInstanceOf(BindingPipe); expect(ast).toBeAnInstanceOf(BindingPipe);
}); });
describe('spans', () => { describe('spans', () => {
it('should should support let', () => { it('should should support let', () => {
const source = 'let i'; const source = 'let i';
expect(keySpans(source, parseTemplateBindings(source))).toEqual(['let i']); expect(keySpans(source, parseTemplateBindings('key', 'let i'))).toEqual(['', 'let i']);
}); });
it('should support multiple lets', () => { it('should support multiple lets', () => {
const source = 'let item; let i=index; let e=even;'; const source = 'let item; let i=index; let e=even;';
expect(keySpans(source, parseTemplateBindings(source))).toEqual([ expect(keySpans(source, parseTemplateBindings('key', source))).toEqual([
'let item', 'let i=index', 'let e=even' '', 'let item', 'let i=index', 'let e=even'
]); ]);
}); });
it('should support a prefix', () => { it('should support a prefix', () => {
const source = 'let person of people'; const source = 'let person of people';
const prefix = 'ngFor'; const prefix = 'ngFor';
const bindings = parseTemplateBindings(source, null, prefix); const bindings = parseTemplateBindings(prefix, source);
expect(keyValues(bindings)).toEqual([ expect(keyValues(bindings)).toEqual([
'ngFor', 'let person=$implicit', 'ngForOf=people in null' 'ngFor', 'let person=$implicit', 'ngForOf=people in null'
]); ]);
@ -506,8 +426,7 @@ import {validate} from './validator';
'Parser Error: Blank expressions are not allowed in interpolated strings'); 'Parser Error: Blank expressions are not allowed in interpolated strings');
}); });
it('should parse conditional expression', it('should parse conditional expression', () => { checkInterpolation('{{ a < b ? a : b }}'); });
() => { checkInterpolation('{{ a < b ? a : b }}'); });
it('should parse expression with newline characters', () => { it('should parse expression with newline characters', () => {
checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`); checkInterpolation(`{{ 'foo' +\n 'bar' +\r 'baz' }}`, `{{ "foo" + "bar" + "baz" }}`);
@ -515,8 +434,7 @@ import {validate} from './validator';
it('should support custom interpolation', () => { it('should support custom interpolation', () => {
const parser = new Parser(new Lexer()); const parser = new Parser(new Lexer());
const ast = const ast = parser.parseInterpolation('{% a %}', null, {start: '{%', end: '%}'}) !.ast as any;
parser.parseInterpolation('{% a %}', null, {start: '{%', end: '%}'}) !.ast as any;
expect(ast.strings).toEqual(['', '']); expect(ast.strings).toEqual(['', '']);
expect(ast.expressions.length).toEqual(1); expect(ast.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a'); expect(ast.expressions[0].name).toEqual('a');
@ -574,8 +492,7 @@ import {validate} from './validator';
describe('wrapLiteralPrimitive', () => { describe('wrapLiteralPrimitive', () => {
it('should wrap a literal primitive', () => { it('should wrap a literal primitive', () => {
expect(unparse(validate(createParser().wrapLiteralPrimitive('foo', null)))) expect(unparse(validate(createParser().wrapLiteralPrimitive('foo', null)))).toEqual('"foo"');
.toEqual('"foo"');
}); });
}); });
@ -605,5 +522,77 @@ import {validate} from './validator';
expect(interpolation.expressions.map(e => e.span.start)).toEqual([2, 9, 16]); expect(interpolation.expressions.map(e => e.span.start)).toEqual([2, 9, 16]);
}); });
}); });
}); });
})();
function createParser() {
return new Parser(new Lexer());
}
function parseAction(text: string, location: any = null): ASTWithSource {
return createParser().parseAction(text, location);
}
function parseBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseBinding(text, location);
}
function parseTemplateBindingsResult(
key: string, value: string, location: any = null): TemplateBindingParseResult {
return createParser().parseTemplateBindings(key, value, location);
}
function parseTemplateBindings(
key: string, value: string, location: any = null): TemplateBinding[] {
return parseTemplateBindingsResult(key, value, location).templateBindings;
}
function parseInterpolation(text: string, location: any = null): ASTWithSource|null {
return createParser().parseInterpolation(text, location);
}
function splitInterpolation(text: string, location: any = null): SplitInterpolation|null {
return createParser().splitInterpolation(text, location);
}
function parseSimpleBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseSimpleBinding(text, location);
}
function checkInterpolation(exp: string, expected?: string) {
const ast = parseInterpolation(exp) !;
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkBinding(exp: string, expected?: string) {
const ast = parseBinding(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkAction(exp: string, expected?: string) {
const ast = parseAction(exp);
if (expected == null) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function expectError(ast: {errors: ParserError[]}, message: string) {
for (const error of ast.errors) {
if (error.message.indexOf(message) >= 0) {
return;
}
}
const errMsgs = ast.errors.map(err => err.message).join('\n');
throw Error(
`Expected an error containing "${message}" to be reported, but got the errors:\n` + errMsgs);
}
function expectActionError(text: string, message: string) {
expectError(validate(parseAction(text)), message);
}
function expectBindingError(text: string, message: string) {
expectError(validate(parseBinding(text)), message);
}