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
a43ed79ee7
. 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:
parent
cf157b99d3
commit
b6ec2387b3
|
@ -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); }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue