Skip to content

Commit

Permalink
feat(templates): introduce quoted expressions to support 3rd-party ex…
Browse files Browse the repository at this point in the history
…pression 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
a43ed79. 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.
  • Loading branch information
yjbanov authored and vsavkin committed Nov 25, 2015
1 parent cf157b9 commit b6ec238
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 15 deletions.
27 changes: 27 additions & 0 deletions modules/angular2/src/core/change_detection/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
52 changes: 38 additions & 14 deletions modules/angular2/src/core/change_detection/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
FunctionCall,
TemplateBinding,
ASTWithSource,
AstVisitor
AstVisitor,
Quote
} from './ast';


Expand Down Expand Up @@ -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[] {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -664,4 +686,6 @@ class SimpleExpressionChecker implements AstVisitor {
}

visitChain(ast: Chain) { this.simple = false; }

visitQuote(ast: Quote) { this.simple = false; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
LiteralPrimitive,
MethodCall,
PrefixNot,
Quote,
SafePropertyRead,
SafeMethodCall
} from './parser/ast';
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
LiteralPrimitive,
MethodCall,
PrefixNot,
Quote,
SafePropertyRead,
SafeMethodCall
} from 'angular2/src/core/change_detection/parser/ast';
Expand Down Expand Up @@ -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); }
}

0 comments on commit b6ec238

Please sign in to comment.