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"; }
|
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 {
|
export class EmptyExpr extends AST {
|
||||||
visit(visitor: AstVisitor) {
|
visit(visitor: AstVisitor) {
|
||||||
// do nothing
|
// do nothing
|
||||||
|
@ -138,6 +159,7 @@ export interface AstVisitor {
|
||||||
visitPrefixNot(ast: PrefixNot): any;
|
visitPrefixNot(ast: PrefixNot): any;
|
||||||
visitPropertyRead(ast: PropertyRead): any;
|
visitPropertyRead(ast: PropertyRead): any;
|
||||||
visitPropertyWrite(ast: PropertyWrite): any;
|
visitPropertyWrite(ast: PropertyWrite): any;
|
||||||
|
visitQuote(ast: Quote): any;
|
||||||
visitSafeMethodCall(ast: SafeMethodCall): any;
|
visitSafeMethodCall(ast: SafeMethodCall): any;
|
||||||
visitSafePropertyRead(ast: SafePropertyRead): any;
|
visitSafePropertyRead(ast: SafePropertyRead): any;
|
||||||
}
|
}
|
||||||
|
@ -210,6 +232,7 @@ export class RecursiveAstVisitor implements AstVisitor {
|
||||||
asts.forEach(ast => ast.visit(this));
|
asts.forEach(ast => ast.visit(this));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
visitQuote(ast: Quote): any { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AstTransformer implements AstVisitor {
|
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)); }
|
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,
|
FunctionCall,
|
||||||
TemplateBinding,
|
TemplateBinding,
|
||||||
ASTWithSource,
|
ASTWithSource,
|
||||||
AstVisitor
|
AstVisitor,
|
||||||
|
Quote
|
||||||
} from './ast';
|
} from './ast';
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,17 +74,46 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
parseBinding(input: string, location: any): ASTWithSource {
|
parseBinding(input: string, location: any): ASTWithSource {
|
||||||
this._checkNoInterpolation(input, location);
|
var ast = this._parseBindingAst(input, location);
|
||||||
var tokens = this._lexer.tokenize(input);
|
|
||||||
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
|
|
||||||
return new ASTWithSource(ast, input, location);
|
return new ASTWithSource(ast, input, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSimpleBinding(input: string, location: string): ASTWithSource {
|
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);
|
this._checkNoInterpolation(input, location);
|
||||||
var tokens = this._lexer.tokenize(input);
|
var tokens = this._lexer.tokenize(input);
|
||||||
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseSimpleBinding();
|
return new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
|
||||||
return new ASTWithSource(ast, input, location);
|
}
|
||||||
|
|
||||||
|
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[] {
|
parseTemplateBindings(input: string, location: any): TemplateBinding[] {
|
||||||
|
@ -216,14 +246,6 @@ export class _ParseAST {
|
||||||
return n.toString();
|
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 {
|
parseChain(): AST {
|
||||||
var exprs = [];
|
var exprs = [];
|
||||||
while (this.index < this.tokens.length) {
|
while (this.index < this.tokens.length) {
|
||||||
|
@ -664,4 +686,6 @@ class SimpleExpressionChecker implements AstVisitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
visitChain(ast: Chain) { this.simple = false; }
|
visitChain(ast: Chain) { this.simple = false; }
|
||||||
|
|
||||||
|
visitQuote(ast: Quote) { this.simple = false; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
LiteralPrimitive,
|
LiteralPrimitive,
|
||||||
MethodCall,
|
MethodCall,
|
||||||
PrefixNot,
|
PrefixNot,
|
||||||
|
Quote,
|
||||||
SafePropertyRead,
|
SafePropertyRead,
|
||||||
SafeMethodCall
|
SafeMethodCall
|
||||||
} from './parser/ast';
|
} from './parser/ast';
|
||||||
|
@ -291,6 +292,12 @@ class _ConvertAstIntoProtoRecords implements AstVisitor {
|
||||||
return this._addRecord(RecordType.Chain, "chain", null, args, null, 0);
|
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[]) {
|
private _visitAll(asts: any[]) {
|
||||||
var res = ListWrapper.createFixedSize(asts.length);
|
var res = ListWrapper.createFixedSize(asts.length);
|
||||||
for (var i = 0; i < asts.length; ++i) {
|
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"|1234').toThrowError(new RegExp('identifier or keyword'));
|
||||||
expectBindingError('"Foo"|"uppercase"').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',
|
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", () => {
|
it("should throw when the given expression is not just a field name", () => {
|
||||||
expect(() => parseSimpleBinding("name + 1"))
|
expect(() => parseSimpleBinding("name + 1"))
|
||||||
.toThrowErrorWith(
|
.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', () => {
|
it('should throw when encountering interpolation', () => {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
LiteralPrimitive,
|
LiteralPrimitive,
|
||||||
MethodCall,
|
MethodCall,
|
||||||
PrefixNot,
|
PrefixNot,
|
||||||
|
Quote,
|
||||||
SafePropertyRead,
|
SafePropertyRead,
|
||||||
SafeMethodCall
|
SafeMethodCall
|
||||||
} from 'angular2/src/core/change_detection/parser/ast';
|
} from 'angular2/src/core/change_detection/parser/ast';
|
||||||
|
@ -187,5 +188,7 @@ export class Unparser implements AstVisitor {
|
||||||
this._expression += ')';
|
this._expression += ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visitQuote(ast: Quote) { this._expression += `${ast.prefix}:${ast.uninterpretedExpression}`; }
|
||||||
|
|
||||||
private _visit(ast: AST) { ast.visit(this); }
|
private _visit(ast: AST) { ast.visit(this); }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue