feat(templates): introduce quoted expressions to support 3rd-party expression languages

A quoted expression is:

quoted expression = prefix `:` uninterpretedExpression
prefix = identifier
uninterpretedExpression = arbitrary string

Example: "route:/some/route"

Quoted expressions are parsed into a new AST node type Quote. The `prefix` part of the
node must be a legal identifier. The `uninterpretedExpression` part of the node is an
arbitrary string that Angular does not interpret.

This feature is meant to be used together with template AST transformers introduced in
https://github.com/angular/angular/commit/a43ed79ee7d49ec55a0adea9b587ed67780c870c. The
transformer would interpret the quoted expression and convert it into a standard AST no
longer containing quoted expressions. Angular will continue compiling the resulting AST
normally.
This commit is contained in:
Yegor Jbanov
2015-11-23 17:58:12 -08:00
committed by vsavkin
parent cf157b99d3
commit b6ec2387b3
5 changed files with 85 additions and 15 deletions
@@ -5,6 +5,27 @@ export class AST {
toString(): string { return "AST"; }
}
/**
* Represents a quoted expression of the form:
*
* quote = prefix `:` uninterpretedExpression
* prefix = identifier
* uninterpretedExpression = arbitrary string
*
* A quoted expression is meant to be pre-processed by an AST transformer that
* converts it into another AST that no longer contains quoted expressions.
* It is meant to allow third-party developers to extend Angular template
* expression language. The `uninterpretedExpression` part of the quote is
* therefore not interpreted by the Angular's own expression parser.
*/
export class Quote extends AST {
constructor(public prefix: string, public uninterpretedExpression: string, public location: any) {
super();
}
visit(visitor: AstVisitor): any { return visitor.visitQuote(this); }
toString(): string { return "Quote"; }
}
export class EmptyExpr extends AST {
visit(visitor: AstVisitor) {
// do nothing
@@ -138,6 +159,7 @@ export interface AstVisitor {
visitPrefixNot(ast: PrefixNot): any;
visitPropertyRead(ast: PropertyRead): any;
visitPropertyWrite(ast: PropertyWrite): any;
visitQuote(ast: Quote): any;
visitSafeMethodCall(ast: SafeMethodCall): any;
visitSafePropertyRead(ast: SafePropertyRead): any;
}
@@ -210,6 +232,7 @@ export class RecursiveAstVisitor implements AstVisitor {
asts.forEach(ast => ast.visit(this));
return null;
}
visitQuote(ast: Quote): any { return null; }
}
export class AstTransformer implements AstVisitor {
@@ -285,4 +308,8 @@ export class AstTransformer implements AstVisitor {
}
visitChain(ast: Chain): AST { return new Chain(this.visitAll(ast.expressions)); }
visitQuote(ast: Quote): AST {
return new Quote(ast.prefix, ast.uninterpretedExpression, ast.location);
}
}
@@ -41,7 +41,8 @@ import {
FunctionCall,
TemplateBinding,
ASTWithSource,
AstVisitor
AstVisitor,
Quote
} from './ast';
@@ -73,17 +74,46 @@ export class Parser {
}
parseBinding(input: string, location: any): ASTWithSource {
this._checkNoInterpolation(input, location);
var tokens = this._lexer.tokenize(input);
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
var ast = this._parseBindingAst(input, location);
return new ASTWithSource(ast, input, location);
}
parseSimpleBinding(input: string, location: string): ASTWithSource {
var ast = this._parseBindingAst(input, location);
if (!SimpleExpressionChecker.check(ast)) {
throw new ParseException(
'Host binding expression can only contain field access and constants', input, location);
}
return new ASTWithSource(ast, input, location);
}
private _parseBindingAst(input: string, location: string): AST {
// Quotes expressions use 3rd-party expression language. We don't want to use
// our lexer or parser for that, so we check for that ahead of time.
var quote = this._parseQuote(input, location);
if (isPresent(quote)) {
return quote;
}
this._checkNoInterpolation(input, location);
var tokens = this._lexer.tokenize(input);
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseSimpleBinding();
return new ASTWithSource(ast, input, location);
return new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
}
private _parseQuote(input: string, location: any): AST {
if (isBlank(input)) return null;
var prefixSeparatorIndex = input.indexOf(':');
if (prefixSeparatorIndex == -1) return null;
var prefix = input.substring(0, prefixSeparatorIndex);
var uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
// while we do not interpret the expression, we do interpret the prefix
var prefixTokens = this._lexer.tokenize(prefix);
// quote prefix must be a single legal identifier
if (prefixTokens.length != 1 || !prefixTokens[0].isIdentifier()) return null;
return new Quote(prefixTokens[0].strValue, uninterpretedExpression, location);
}
parseTemplateBindings(input: string, location: any): TemplateBinding[] {
@@ -216,14 +246,6 @@ export class _ParseAST {
return n.toString();
}
parseSimpleBinding(): AST {
var ast = this.parseChain();
if (!SimpleExpressionChecker.check(ast)) {
this.error(`Simple binding expression can only contain field access and constants'`);
}
return ast;
}
parseChain(): AST {
var exprs = [];
while (this.index < this.tokens.length) {
@@ -664,4 +686,6 @@ class SimpleExpressionChecker implements AstVisitor {
}
visitChain(ast: Chain) { this.simple = false; }
visitQuote(ast: Quote) { this.simple = false; }
}
@@ -22,6 +22,7 @@ import {
LiteralPrimitive,
MethodCall,
PrefixNot,
Quote,
SafePropertyRead,
SafeMethodCall
} from './parser/ast';
@@ -291,6 +292,12 @@ class _ConvertAstIntoProtoRecords implements AstVisitor {
return this._addRecord(RecordType.Chain, "chain", null, args, null, 0);
}
visitQuote(ast: Quote): void {
throw new BaseException(
`Caught uninterpreted expression at ${ast.location}: ${ast.uninterpretedExpression}. ` +
`Expression prefix ${ast.prefix} did not match a template transformer to interpret the expression.`);
}
private _visitAll(asts: any[]) {
var res = ListWrapper.createFixedSize(asts.length);
for (var i = 0; i < asts.length; ++i) {
@@ -230,6 +230,15 @@ export function main() {
expectBindingError('"Foo"|1234').toThrowError(new RegExp('identifier or keyword'));
expectBindingError('"Foo"|"uppercase"').toThrowError(new RegExp('identifier or keyword'));
});
it('should parse quoted expressions', () => { checkBinding('a:b', 'a:b'); });
it('should ignore whitespace around quote prefix', () => { checkBinding(' a :b', 'a:b'); });
it('should refuse prefixes that are not single identifiers', () => {
expectBindingError('a + b:c').toThrowError();
expectBindingError('1:c').toThrowError();
});
});
it('should store the source in the result',
@@ -414,7 +423,7 @@ export function main() {
it("should throw when the given expression is not just a field name", () => {
expect(() => parseSimpleBinding("name + 1"))
.toThrowErrorWith(
'Simple binding expression can only contain field access and constants');
'Host binding expression can only contain field access and constants');
});
it('should throw when encountering interpolation', () => {
@@ -18,6 +18,7 @@ import {
LiteralPrimitive,
MethodCall,
PrefixNot,
Quote,
SafePropertyRead,
SafeMethodCall
} from 'angular2/src/core/change_detection/parser/ast';
@@ -187,5 +188,7 @@ export class Unparser implements AstVisitor {
this._expression += ')';
}
visitQuote(ast: Quote) { this._expression += `${ast.prefix}:${ast.uninterpretedExpression}`; }
private _visit(ast: AST) { ast.visit(this); }
}