Skip to content

Commit c6846f1

Browse files
committed
feat(compiler): new semantics for template attributes and view variables.
- Supports `<div template=“…”>`, including parsing the expressions within the attribute. - Supports `<template let-ng-repeat=“rows”>` - Adds attribute interpolation (was missing previously)
1 parent f864aa1 commit c6846f1

25 files changed

+579
-276
lines changed

modules/change_detection/src/parser/ast.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class KeyedAccess extends AST {
111111
this.obj = obj;
112112
this.key = key;
113113
}
114-
114+
115115
eval(context) {
116116
var obj = this.obj.eval(context);
117117
var key = this.key.eval(context);
@@ -169,11 +169,11 @@ export class LiteralPrimitive extends AST {
169169
constructor(value) {
170170
this.value = value;
171171
}
172-
172+
173173
eval(context) {
174174
return this.value;
175175
}
176-
176+
177177
visit(visitor, args) {
178178
visitor.visitLiteralPrimitive(this, args);
179179
}
@@ -184,11 +184,11 @@ export class LiteralArray extends AST {
184184
constructor(expressions:List) {
185185
this.expressions = expressions;
186186
}
187-
187+
188188
eval(context) {
189189
return ListWrapper.map(this.expressions, (e) => e.eval(context));
190190
}
191-
191+
192192
visit(visitor, args) {
193193
visitor.visitLiteralArray(this, args);
194194
}
@@ -287,7 +287,7 @@ export class Assignment extends AST {
287287
eval(context) {
288288
return this.target.assign(context, this.value.eval(context));
289289
}
290-
290+
291291
visit(visitor, args) {
292292
visitor.visitAssignment(this, args);
293293
}
@@ -336,6 +336,22 @@ export class FunctionCall extends AST {
336336
}
337337
}
338338

339+
export class ASTWithSource {
340+
constructor(ast:AST, source:string) {
341+
this.source = source;
342+
this.ast = ast;
343+
}
344+
}
345+
346+
export class TemplateBinding {
347+
constructor(key:string, name:string, expression:ASTWithSource) {
348+
this.key = key;
349+
// only either name or expression will be filled.
350+
this.name = name;
351+
this.expression = expression;
352+
}
353+
}
354+
339355
//INTERFACE
340356
export class AstVisitor {
341357
visitChain(ast:Chain, args){}

modules/change_detection/src/parser/lexer.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export const $CR = 13;
130130
export const $SPACE = 32;
131131
export const $BANG = 33;
132132
export const $DQ = 34;
133+
export const $HASH = 35;
133134
export const $$ = 36;
134135
export const $PERCENT = 37;
135136
export const $AMPERSAND = 38;
@@ -246,6 +247,8 @@ class _Scanner {
246247
case $SQ:
247248
case $DQ:
248249
return this.scanString();
250+
case $HASH:
251+
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
249252
case $PLUS:
250253
case $MINUS:
251254
case $STAR:
@@ -459,7 +462,8 @@ var OPERATORS = SetWrapper.createFromList([
459462
'&',
460463
'|',
461464
'!',
462-
'?'
465+
'?',
466+
'#'
463467
]);
464468

465469

modules/change_detection/src/parser/parser.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
LiteralArray,
2020
LiteralMap,
2121
MethodCall,
22-
FunctionCall
22+
FunctionCall,
23+
TemplateBindings,
24+
TemplateBinding,
25+
ASTWithSource
2326
} from './ast';
2427

2528
var _implicitReceiver = new ImplicitReceiver();
@@ -32,14 +35,21 @@ export class Parser {
3235
this._closureMap = closureMap;
3336
}
3437

35-
parseAction(input:string):AST {
38+
parseAction(input:string):ASTWithSource {
3639
var tokens = this._lexer.tokenize(input);
37-
return new _ParseAST(input, tokens, this._closureMap, true).parseChain();
40+
var ast = new _ParseAST(input, tokens, this._closureMap, true).parseChain();
41+
return new ASTWithSource(ast, input);
3842
}
3943

40-
parseBinding(input:string):AST {
44+
parseBinding(input:string):ASTWithSource {
4145
var tokens = this._lexer.tokenize(input);
42-
return new _ParseAST(input, tokens, this._closureMap, false).parseChain();
46+
var ast = new _ParseAST(input, tokens, this._closureMap, false).parseChain();
47+
return new ASTWithSource(ast, input);
48+
}
49+
50+
parseTemplateBindings(input:string):List<TemplateBinding> {
51+
var tokens = this._lexer.tokenize(input);
52+
return new _ParseAST(input, tokens, this._closureMap, false).parseTemplateBindings();
4353
}
4454
}
4555

@@ -407,6 +417,29 @@ class _ParseAST {
407417
return positionals;
408418
}
409419

420+
parseTemplateBindings() {
421+
var bindings = [];
422+
while (this.index < this.tokens.length) {
423+
var key = this.expectIdentifierOrKeywordOrString();
424+
this.optionalCharacter($COLON);
425+
var name = null;
426+
var expression = null;
427+
if (this.optionalOperator("#")) {
428+
name = this.expectIdentifierOrKeyword();
429+
} else {
430+
var start = this.inputIndex;
431+
var ast = this.parseExpression();
432+
var source = this.input.substring(start, this.inputIndex);
433+
expression = new ASTWithSource(ast, source);
434+
}
435+
ListWrapper.push(bindings, new TemplateBinding(key, name, expression));
436+
if (!this.optionalCharacter($SEMICOLON)) {
437+
this.optionalCharacter($COMMA);
438+
};
439+
}
440+
return bindings;
441+
}
442+
410443
error(message:string, index:int = null) {
411444
if (isBlank(index)) index = this.index;
412445

modules/change_detection/src/watch_group.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ class ProtoRecordCreator {
310310

311311
visitAssignment(ast:Assignment, dest) {this.unsupported();}
312312

313+
visitTemplateBindings(ast, dest) {this.unsupported();}
314+
313315
createRecordsFromAST(ast:AST, memento){
314316
ast.visit(this, memento);
315317
}

modules/change_detection/test/change_detector_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {Record} from 'change_detection/record';
1818
export function main() {
1919
function ast(exp:string) {
2020
var parser = new Parser(new Lexer(), new ClosureMap());
21-
return parser.parseBinding(exp);
21+
return parser.parseBinding(exp).ast;
2222
}
2323

2424
function createChangeDetector(memo:string, exp:string, context = null, formatters = null) {

modules/change_detection/test/parser/lexer_spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ export function main() {
237237
}).toThrowError("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']");
238238
});
239239

240+
it('should tokenize hash as operator', function() {
241+
var tokens:List<Token> = lex("#");
242+
expectOperatorToken(tokens[0], 0, '#');
243+
});
244+
240245
});
241246
});
242247
}

modules/change_detection/test/parser/parser_spec.js

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'test_lib/test_lib';
2-
import {BaseException, isBlank} from 'facade/lang';
3-
import {MapWrapper} from 'facade/collection';
2+
import {BaseException, isBlank, isPresent} from 'facade/lang';
3+
import {MapWrapper, ListWrapper} from 'facade/collection';
44
import {Parser} from 'change_detection/parser/parser';
55
import {Lexer} from 'change_detection/parser/lexer';
66
import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast';
@@ -32,11 +32,15 @@ export function main() {
3232
}
3333

3434
function parseAction(text) {
35-
return createParser().parseAction(text);
35+
return createParser().parseAction(text).ast;
3636
}
3737

3838
function parseBinding(text) {
39-
return createParser().parseBinding(text);
39+
return createParser().parseBinding(text).ast;
40+
}
41+
42+
function parseTemplateBindings(text) {
43+
return createParser().parseTemplateBindings(text);
4044
}
4145

4246
function expectEval(text, passedInContext = null) {
@@ -48,6 +52,15 @@ export function main() {
4852
return expect(() => parseAction(text).eval(td()));
4953
}
5054

55+
function evalAsts(asts, passedInContext = null) {
56+
var c = isBlank(passedInContext) ? td() : passedInContext;
57+
var res = [];
58+
for (var i=0; i<asts.length; i++) {
59+
ListWrapper.push(res, asts[i].eval(c));
60+
}
61+
return res;
62+
}
63+
5164
describe("parser", () => {
5265
describe("parseAction", () => {
5366
describe("basic expressions", () => {
@@ -248,7 +261,7 @@ export function main() {
248261
expectEval('a["key"] = 200', context).toEqual(200);
249262
expect(MapWrapper.get(context.a, "key")).toEqual(200);
250263
});
251-
264+
252265
it("should support array/map updates", () => {
253266
var context = td([MapWrapper.createFromPairs([["key", 100]])]);
254267
expectEval('a[0]["key"] = 200', context).toEqual(200);
@@ -287,7 +300,7 @@ export function main() {
287300

288301
it('should pass exceptions', () => {
289302
expect(() => {
290-
createParser().parseAction('a()').eval(td(() => {throw new BaseException("boo to you")}));
303+
createParser().parseAction('a()').ast.eval(td(() => {throw new BaseException("boo to you")}));
291304
}).toThrowError('boo to you');
292305
});
293306

@@ -297,6 +310,10 @@ export function main() {
297310
expectEval("1;;").toEqual(1);
298311
});
299312
});
313+
314+
it('should store the source in the result', () => {
315+
expect(createParser().parseAction('someExpr').source).toBe('someExpr');
316+
});
300317
});
301318

302319
describe("parseBinding", () => {
@@ -319,6 +336,11 @@ export function main() {
319336
expect(() => parseBinding('"Foo"|1234')).toThrowError(new RegExp('identifier or keyword'));
320337
expect(() => parseBinding('"Foo"|"uppercase"')).toThrowError(new RegExp('identifier or keyword'));
321338
});
339+
340+
});
341+
342+
it('should store the source in the result', () => {
343+
expect(createParser().parseBinding('someExpr').source).toBe('someExpr');
322344
});
323345

324346
it('should throw on chain expressions', () => {
@@ -329,6 +351,90 @@ export function main() {
329351
expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression"));
330352
});
331353
});
354+
355+
describe('parseTemplateBindings', () => {
356+
357+
function keys(templateBindings) {
358+
return ListWrapper.map(templateBindings, (binding) => binding.key );
359+
}
360+
361+
function names(templateBindings) {
362+
return ListWrapper.map(templateBindings, (binding) => binding.name );
363+
}
364+
365+
function exprSources(templateBindings) {
366+
return ListWrapper.map(templateBindings,
367+
(binding) => isPresent(binding.expression) ? binding.expression.source : null );
368+
}
369+
370+
function exprAsts(templateBindings) {
371+
return ListWrapper.map(templateBindings,
372+
(binding) => isPresent(binding.expression) ? binding.expression.ast : null );
373+
}
374+
375+
it('should parse an empty string', () => {
376+
var bindings = parseTemplateBindings("");
377+
expect(bindings).toEqual([]);
378+
});
379+
380+
it('should only allow identifier, string, or keyword as keys', () => {
381+
var bindings = parseTemplateBindings("a:'b'");
382+
expect(keys(bindings)).toEqual(['a']);
383+
384+
bindings = parseTemplateBindings("'a':'b'");
385+
expect(keys(bindings)).toEqual(['a']);
386+
387+
bindings = parseTemplateBindings("\"a\":'b'");
388+
expect(keys(bindings)).toEqual(['a']);
389+
390+
expect( () => {
391+
parseTemplateBindings('(:0');
392+
}).toThrowError(new RegExp('expected identifier, keyword, or string'));
393+
394+
expect( () => {
395+
parseTemplateBindings('1234:0');
396+
}).toThrowError(new RegExp('expected identifier, keyword, or string'));
397+
});
398+
399+
it('should detect expressions as value', () => {
400+
var bindings = parseTemplateBindings("a:b");
401+
expect(exprSources(bindings)).toEqual(['b']);
402+
expect(evalAsts(exprAsts(bindings), td(0, 23))).toEqual([23]);
403+
404+
bindings = parseTemplateBindings("a:1+1");
405+
expect(exprSources(bindings)).toEqual(['1+1']);
406+
expect(evalAsts(exprAsts(bindings))).toEqual([2]);
407+
});
408+
409+
it('should detect names as value', () => {
410+
var bindings = parseTemplateBindings("a:#b");
411+
expect(names(bindings)).toEqual(['b']);
412+
expect(exprSources(bindings)).toEqual([null]);
413+
expect(exprAsts(bindings)).toEqual([null]);
414+
});
415+
416+
it('should allow space and colon as separators', () => {
417+
var bindings = parseTemplateBindings("a:b");
418+
expect(keys(bindings)).toEqual(['a']);
419+
expect(exprSources(bindings)).toEqual(['b']);
420+
421+
bindings = parseTemplateBindings("a b");
422+
expect(keys(bindings)).toEqual(['a']);
423+
expect(exprSources(bindings)).toEqual(['b']);
424+
});
425+
426+
it('should allow multiple pairs', () => {
427+
var bindings = parseTemplateBindings("a 1 b 2");
428+
expect(keys(bindings)).toEqual(['a', 'b']);
429+
expect(exprSources(bindings)).toEqual(['1 ', '2']);
430+
});
431+
432+
it('should store the sources in the result', () => {
433+
var bindings = parseTemplateBindings("a 1,b 2");
434+
expect(bindings[0].expression.source).toEqual('1');
435+
expect(bindings[1].expression.source).toEqual('2');
436+
});
437+
});
332438
});
333439
}
334440

0 commit comments

Comments
 (0)