X Tutup
Skip to content

Commit b6ec238

Browse files
yjbanovvsavkin
authored andcommitted
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 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.
1 parent cf157b9 commit b6ec238

File tree

5 files changed

+85
-15
lines changed

5 files changed

+85
-15
lines changed

modules/angular2/src/core/change_detection/parser/ast.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ export class AST {
55
toString(): string { return "AST"; }
66
}
77

8+
/**
9+
* Represents a quoted expression of the form:
10+
*
11+
* quote = prefix `:` uninterpretedExpression
12+
* prefix = identifier
13+
* uninterpretedExpression = arbitrary string
14+
*
15+
* A quoted expression is meant to be pre-processed by an AST transformer that
16+
* converts it into another AST that no longer contains quoted expressions.
17+
* It is meant to allow third-party developers to extend Angular template
18+
* expression language. The `uninterpretedExpression` part of the quote is
19+
* therefore not interpreted by the Angular's own expression parser.
20+
*/
21+
export class Quote extends AST {
22+
constructor(public prefix: string, public uninterpretedExpression: string, public location: any) {
23+
super();
24+
}
25+
visit(visitor: AstVisitor): any { return visitor.visitQuote(this); }
26+
toString(): string { return "Quote"; }
27+
}
28+
829
export class EmptyExpr extends AST {
930
visit(visitor: AstVisitor) {
1031
// do nothing
@@ -138,6 +159,7 @@ export interface AstVisitor {
138159
visitPrefixNot(ast: PrefixNot): any;
139160
visitPropertyRead(ast: PropertyRead): any;
140161
visitPropertyWrite(ast: PropertyWrite): any;
162+
visitQuote(ast: Quote): any;
141163
visitSafeMethodCall(ast: SafeMethodCall): any;
142164
visitSafePropertyRead(ast: SafePropertyRead): any;
143165
}
@@ -210,6 +232,7 @@ export class RecursiveAstVisitor implements AstVisitor {
210232
asts.forEach(ast => ast.visit(this));
211233
return null;
212234
}
235+
visitQuote(ast: Quote): any { return null; }
213236
}
214237

215238
export class AstTransformer implements AstVisitor {
@@ -285,4 +308,8 @@ export class AstTransformer implements AstVisitor {
285308
}
286309

287310
visitChain(ast: Chain): AST { return new Chain(this.visitAll(ast.expressions)); }
311+
312+
visitQuote(ast: Quote): AST {
313+
return new Quote(ast.prefix, ast.uninterpretedExpression, ast.location);
314+
}
288315
}

modules/angular2/src/core/change_detection/parser/parser.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ import {
4141
FunctionCall,
4242
TemplateBinding,
4343
ASTWithSource,
44-
AstVisitor
44+
AstVisitor,
45+
Quote
4546
} from './ast';
4647

4748

@@ -73,17 +74,46 @@ export class Parser {
7374
}
7475

7576
parseBinding(input: string, location: any): ASTWithSource {
76-
this._checkNoInterpolation(input, location);
77-
var tokens = this._lexer.tokenize(input);
78-
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
77+
var ast = this._parseBindingAst(input, location);
7978
return new ASTWithSource(ast, input, location);
8079
}
8180

8281
parseSimpleBinding(input: string, location: string): ASTWithSource {
82+
var ast = this._parseBindingAst(input, location);
83+
if (!SimpleExpressionChecker.check(ast)) {
84+
throw new ParseException(
85+
'Host binding expression can only contain field access and constants', input, location);
86+
}
87+
return new ASTWithSource(ast, input, location);
88+
}
89+
90+
private _parseBindingAst(input: string, location: string): AST {
91+
// Quotes expressions use 3rd-party expression language. We don't want to use
92+
// our lexer or parser for that, so we check for that ahead of time.
93+
var quote = this._parseQuote(input, location);
94+
95+
if (isPresent(quote)) {
96+
return quote;
97+
}
98+
8399
this._checkNoInterpolation(input, location);
84100
var tokens = this._lexer.tokenize(input);
85-
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseSimpleBinding();
86-
return new ASTWithSource(ast, input, location);
101+
return new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
102+
}
103+
104+
private _parseQuote(input: string, location: any): AST {
105+
if (isBlank(input)) return null;
106+
var prefixSeparatorIndex = input.indexOf(':');
107+
if (prefixSeparatorIndex == -1) return null;
108+
var prefix = input.substring(0, prefixSeparatorIndex);
109+
var uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
110+
111+
// while we do not interpret the expression, we do interpret the prefix
112+
var prefixTokens = this._lexer.tokenize(prefix);
113+
114+
// quote prefix must be a single legal identifier
115+
if (prefixTokens.length != 1 || !prefixTokens[0].isIdentifier()) return null;
116+
return new Quote(prefixTokens[0].strValue, uninterpretedExpression, location);
87117
}
88118

89119
parseTemplateBindings(input: string, location: any): TemplateBinding[] {
@@ -216,14 +246,6 @@ export class _ParseAST {
216246
return n.toString();
217247
}
218248

219-
parseSimpleBinding(): AST {
220-
var ast = this.parseChain();
221-
if (!SimpleExpressionChecker.check(ast)) {
222-
this.error(`Simple binding expression can only contain field access and constants'`);
223-
}
224-
return ast;
225-
}
226-
227249
parseChain(): AST {
228250
var exprs = [];
229251
while (this.index < this.tokens.length) {
@@ -664,4 +686,6 @@ class SimpleExpressionChecker implements AstVisitor {
664686
}
665687

666688
visitChain(ast: Chain) { this.simple = false; }
689+
690+
visitQuote(ast: Quote) { this.simple = false; }
667691
}

modules/angular2/src/core/change_detection/proto_change_detector.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
LiteralPrimitive,
2323
MethodCall,
2424
PrefixNot,
25+
Quote,
2526
SafePropertyRead,
2627
SafeMethodCall
2728
} from './parser/ast';
@@ -291,6 +292,12 @@ class _ConvertAstIntoProtoRecords implements AstVisitor {
291292
return this._addRecord(RecordType.Chain, "chain", null, args, null, 0);
292293
}
293294

295+
visitQuote(ast: Quote): void {
296+
throw new BaseException(
297+
`Caught uninterpreted expression at ${ast.location}: ${ast.uninterpretedExpression}. ` +
298+
`Expression prefix ${ast.prefix} did not match a template transformer to interpret the expression.`);
299+
}
300+
294301
private _visitAll(asts: any[]) {
295302
var res = ListWrapper.createFixedSize(asts.length);
296303
for (var i = 0; i < asts.length; ++i) {

modules/angular2/test/core/change_detection/parser/parser_spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,15 @@ export function main() {
230230
expectBindingError('"Foo"|1234').toThrowError(new RegExp('identifier or keyword'));
231231
expectBindingError('"Foo"|"uppercase"').toThrowError(new RegExp('identifier or keyword'));
232232
});
233+
234+
it('should parse quoted expressions', () => { checkBinding('a:b', 'a:b'); });
235+
236+
it('should ignore whitespace around quote prefix', () => { checkBinding(' a :b', 'a:b'); });
237+
238+
it('should refuse prefixes that are not single identifiers', () => {
239+
expectBindingError('a + b:c').toThrowError();
240+
expectBindingError('1:c').toThrowError();
241+
});
233242
});
234243

235244
it('should store the source in the result',
@@ -414,7 +423,7 @@ export function main() {
414423
it("should throw when the given expression is not just a field name", () => {
415424
expect(() => parseSimpleBinding("name + 1"))
416425
.toThrowErrorWith(
417-
'Simple binding expression can only contain field access and constants');
426+
'Host binding expression can only contain field access and constants');
418427
});
419428

420429
it('should throw when encountering interpolation', () => {

modules/angular2/test/core/change_detection/parser/unparser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
LiteralPrimitive,
1919
MethodCall,
2020
PrefixNot,
21+
Quote,
2122
SafePropertyRead,
2223
SafeMethodCall
2324
} from 'angular2/src/core/change_detection/parser/ast';
@@ -187,5 +188,7 @@ export class Unparser implements AstVisitor {
187188
this._expression += ')';
188189
}
189190

191+
visitQuote(ast: Quote) { this._expression += `${ast.prefix}:${ast.uninterpretedExpression}`; }
192+
190193
private _visit(ast: AST) { ast.visit(this); }
191194
}

0 commit comments

Comments
 (0)
X Tutup