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)
This commit is contained in:
@ -111,7 +111,7 @@ export class KeyedAccess extends AST {
|
||||
this.obj = obj;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
eval(context) {
|
||||
var obj = this.obj.eval(context);
|
||||
var key = this.key.eval(context);
|
||||
@ -169,11 +169,11 @@ export class LiteralPrimitive extends AST {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
eval(context) {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitLiteralPrimitive(this, args);
|
||||
}
|
||||
@ -184,11 +184,11 @@ export class LiteralArray extends AST {
|
||||
constructor(expressions:List) {
|
||||
this.expressions = expressions;
|
||||
}
|
||||
|
||||
|
||||
eval(context) {
|
||||
return ListWrapper.map(this.expressions, (e) => e.eval(context));
|
||||
}
|
||||
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitLiteralArray(this, args);
|
||||
}
|
||||
@ -287,7 +287,7 @@ export class Assignment extends AST {
|
||||
eval(context) {
|
||||
return this.target.assign(context, this.value.eval(context));
|
||||
}
|
||||
|
||||
|
||||
visit(visitor, args) {
|
||||
visitor.visitAssignment(this, args);
|
||||
}
|
||||
@ -336,6 +336,22 @@ export class FunctionCall extends AST {
|
||||
}
|
||||
}
|
||||
|
||||
export class ASTWithSource {
|
||||
constructor(ast:AST, source:string) {
|
||||
this.source = source;
|
||||
this.ast = ast;
|
||||
}
|
||||
}
|
||||
|
||||
export class TemplateBinding {
|
||||
constructor(key:string, name:string, expression:ASTWithSource) {
|
||||
this.key = key;
|
||||
// only either name or expression will be filled.
|
||||
this.name = name;
|
||||
this.expression = expression;
|
||||
}
|
||||
}
|
||||
|
||||
//INTERFACE
|
||||
export class AstVisitor {
|
||||
visitChain(ast:Chain, args){}
|
||||
|
@ -130,6 +130,7 @@ export const $CR = 13;
|
||||
export const $SPACE = 32;
|
||||
export const $BANG = 33;
|
||||
export const $DQ = 34;
|
||||
export const $HASH = 35;
|
||||
export const $$ = 36;
|
||||
export const $PERCENT = 37;
|
||||
export const $AMPERSAND = 38;
|
||||
@ -246,6 +247,8 @@ class _Scanner {
|
||||
case $SQ:
|
||||
case $DQ:
|
||||
return this.scanString();
|
||||
case $HASH:
|
||||
return this.scanOperator(start, StringWrapper.fromCharCode(peek));
|
||||
case $PLUS:
|
||||
case $MINUS:
|
||||
case $STAR:
|
||||
@ -459,7 +462,8 @@ var OPERATORS = SetWrapper.createFromList([
|
||||
'&',
|
||||
'|',
|
||||
'!',
|
||||
'?'
|
||||
'?',
|
||||
'#'
|
||||
]);
|
||||
|
||||
|
||||
|
@ -19,7 +19,10 @@ import {
|
||||
LiteralArray,
|
||||
LiteralMap,
|
||||
MethodCall,
|
||||
FunctionCall
|
||||
FunctionCall,
|
||||
TemplateBindings,
|
||||
TemplateBinding,
|
||||
ASTWithSource
|
||||
} from './ast';
|
||||
|
||||
var _implicitReceiver = new ImplicitReceiver();
|
||||
@ -32,14 +35,21 @@ export class Parser {
|
||||
this._closureMap = closureMap;
|
||||
}
|
||||
|
||||
parseAction(input:string):AST {
|
||||
parseAction(input:string):ASTWithSource {
|
||||
var tokens = this._lexer.tokenize(input);
|
||||
return new _ParseAST(input, tokens, this._closureMap, true).parseChain();
|
||||
var ast = new _ParseAST(input, tokens, this._closureMap, true).parseChain();
|
||||
return new ASTWithSource(ast, input);
|
||||
}
|
||||
|
||||
parseBinding(input:string):AST {
|
||||
parseBinding(input:string):ASTWithSource {
|
||||
var tokens = this._lexer.tokenize(input);
|
||||
return new _ParseAST(input, tokens, this._closureMap, false).parseChain();
|
||||
var ast = new _ParseAST(input, tokens, this._closureMap, false).parseChain();
|
||||
return new ASTWithSource(ast, input);
|
||||
}
|
||||
|
||||
parseTemplateBindings(input:string):List<TemplateBinding> {
|
||||
var tokens = this._lexer.tokenize(input);
|
||||
return new _ParseAST(input, tokens, this._closureMap, false).parseTemplateBindings();
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,6 +417,29 @@ class _ParseAST {
|
||||
return positionals;
|
||||
}
|
||||
|
||||
parseTemplateBindings() {
|
||||
var bindings = [];
|
||||
while (this.index < this.tokens.length) {
|
||||
var key = this.expectIdentifierOrKeywordOrString();
|
||||
this.optionalCharacter($COLON);
|
||||
var name = null;
|
||||
var expression = null;
|
||||
if (this.optionalOperator("#")) {
|
||||
name = this.expectIdentifierOrKeyword();
|
||||
} else {
|
||||
var start = this.inputIndex;
|
||||
var ast = this.parseExpression();
|
||||
var source = this.input.substring(start, this.inputIndex);
|
||||
expression = new ASTWithSource(ast, source);
|
||||
}
|
||||
ListWrapper.push(bindings, new TemplateBinding(key, name, expression));
|
||||
if (!this.optionalCharacter($SEMICOLON)) {
|
||||
this.optionalCharacter($COMMA);
|
||||
};
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
error(message:string, index:int = null) {
|
||||
if (isBlank(index)) index = this.index;
|
||||
|
||||
|
@ -310,6 +310,8 @@ class ProtoRecordCreator {
|
||||
|
||||
visitAssignment(ast:Assignment, dest) {this.unsupported();}
|
||||
|
||||
visitTemplateBindings(ast, dest) {this.unsupported();}
|
||||
|
||||
createRecordsFromAST(ast:AST, memento){
|
||||
ast.visit(this, memento);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import {Record} from 'change_detection/record';
|
||||
export function main() {
|
||||
function ast(exp:string) {
|
||||
var parser = new Parser(new Lexer(), new ClosureMap());
|
||||
return parser.parseBinding(exp);
|
||||
return parser.parseBinding(exp).ast;
|
||||
}
|
||||
|
||||
function createChangeDetector(memo:string, exp:string, context = null, formatters = null) {
|
||||
|
@ -237,6 +237,11 @@ export function main() {
|
||||
}).toThrowError("Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']");
|
||||
});
|
||||
|
||||
it('should tokenize hash as operator', function() {
|
||||
var tokens:List<Token> = lex("#");
|
||||
expectOperatorToken(tokens[0], 0, '#');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {ddescribe, describe, it, xit, iit, expect, beforeEach} from 'test_lib/test_lib';
|
||||
import {BaseException, isBlank} from 'facade/lang';
|
||||
import {MapWrapper} from 'facade/collection';
|
||||
import {BaseException, isBlank, isPresent} from 'facade/lang';
|
||||
import {MapWrapper, ListWrapper} from 'facade/collection';
|
||||
import {Parser} from 'change_detection/parser/parser';
|
||||
import {Lexer} from 'change_detection/parser/lexer';
|
||||
import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast';
|
||||
@ -32,11 +32,15 @@ export function main() {
|
||||
}
|
||||
|
||||
function parseAction(text) {
|
||||
return createParser().parseAction(text);
|
||||
return createParser().parseAction(text).ast;
|
||||
}
|
||||
|
||||
function parseBinding(text) {
|
||||
return createParser().parseBinding(text);
|
||||
return createParser().parseBinding(text).ast;
|
||||
}
|
||||
|
||||
function parseTemplateBindings(text) {
|
||||
return createParser().parseTemplateBindings(text);
|
||||
}
|
||||
|
||||
function expectEval(text, passedInContext = null) {
|
||||
@ -48,6 +52,15 @@ export function main() {
|
||||
return expect(() => parseAction(text).eval(td()));
|
||||
}
|
||||
|
||||
function evalAsts(asts, passedInContext = null) {
|
||||
var c = isBlank(passedInContext) ? td() : passedInContext;
|
||||
var res = [];
|
||||
for (var i=0; i<asts.length; i++) {
|
||||
ListWrapper.push(res, asts[i].eval(c));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
describe("parser", () => {
|
||||
describe("parseAction", () => {
|
||||
describe("basic expressions", () => {
|
||||
@ -248,7 +261,7 @@ export function main() {
|
||||
expectEval('a["key"] = 200', context).toEqual(200);
|
||||
expect(MapWrapper.get(context.a, "key")).toEqual(200);
|
||||
});
|
||||
|
||||
|
||||
it("should support array/map updates", () => {
|
||||
var context = td([MapWrapper.createFromPairs([["key", 100]])]);
|
||||
expectEval('a[0]["key"] = 200', context).toEqual(200);
|
||||
@ -287,7 +300,7 @@ export function main() {
|
||||
|
||||
it('should pass exceptions', () => {
|
||||
expect(() => {
|
||||
createParser().parseAction('a()').eval(td(() => {throw new BaseException("boo to you")}));
|
||||
createParser().parseAction('a()').ast.eval(td(() => {throw new BaseException("boo to you")}));
|
||||
}).toThrowError('boo to you');
|
||||
});
|
||||
|
||||
@ -297,6 +310,10 @@ export function main() {
|
||||
expectEval("1;;").toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should store the source in the result', () => {
|
||||
expect(createParser().parseAction('someExpr').source).toBe('someExpr');
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBinding", () => {
|
||||
@ -319,6 +336,11 @@ export function main() {
|
||||
expect(() => parseBinding('"Foo"|1234')).toThrowError(new RegExp('identifier or keyword'));
|
||||
expect(() => parseBinding('"Foo"|"uppercase"')).toThrowError(new RegExp('identifier or keyword'));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should store the source in the result', () => {
|
||||
expect(createParser().parseBinding('someExpr').source).toBe('someExpr');
|
||||
});
|
||||
|
||||
it('should throw on chain expressions', () => {
|
||||
@ -329,6 +351,90 @@ export function main() {
|
||||
expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression"));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTemplateBindings', () => {
|
||||
|
||||
function keys(templateBindings) {
|
||||
return ListWrapper.map(templateBindings, (binding) => binding.key );
|
||||
}
|
||||
|
||||
function names(templateBindings) {
|
||||
return ListWrapper.map(templateBindings, (binding) => binding.name );
|
||||
}
|
||||
|
||||
function exprSources(templateBindings) {
|
||||
return ListWrapper.map(templateBindings,
|
||||
(binding) => isPresent(binding.expression) ? binding.expression.source : null );
|
||||
}
|
||||
|
||||
function exprAsts(templateBindings) {
|
||||
return ListWrapper.map(templateBindings,
|
||||
(binding) => isPresent(binding.expression) ? binding.expression.ast : null );
|
||||
}
|
||||
|
||||
it('should parse an empty string', () => {
|
||||
var bindings = parseTemplateBindings("");
|
||||
expect(bindings).toEqual([]);
|
||||
});
|
||||
|
||||
it('should only allow identifier, string, or keyword as keys', () => {
|
||||
var bindings = parseTemplateBindings("a:'b'");
|
||||
expect(keys(bindings)).toEqual(['a']);
|
||||
|
||||
bindings = parseTemplateBindings("'a':'b'");
|
||||
expect(keys(bindings)).toEqual(['a']);
|
||||
|
||||
bindings = parseTemplateBindings("\"a\":'b'");
|
||||
expect(keys(bindings)).toEqual(['a']);
|
||||
|
||||
expect( () => {
|
||||
parseTemplateBindings('(:0');
|
||||
}).toThrowError(new RegExp('expected identifier, keyword, or string'));
|
||||
|
||||
expect( () => {
|
||||
parseTemplateBindings('1234:0');
|
||||
}).toThrowError(new RegExp('expected identifier, keyword, or string'));
|
||||
});
|
||||
|
||||
it('should detect expressions as value', () => {
|
||||
var bindings = parseTemplateBindings("a:b");
|
||||
expect(exprSources(bindings)).toEqual(['b']);
|
||||
expect(evalAsts(exprAsts(bindings), td(0, 23))).toEqual([23]);
|
||||
|
||||
bindings = parseTemplateBindings("a:1+1");
|
||||
expect(exprSources(bindings)).toEqual(['1+1']);
|
||||
expect(evalAsts(exprAsts(bindings))).toEqual([2]);
|
||||
});
|
||||
|
||||
it('should detect names as value', () => {
|
||||
var bindings = parseTemplateBindings("a:#b");
|
||||
expect(names(bindings)).toEqual(['b']);
|
||||
expect(exprSources(bindings)).toEqual([null]);
|
||||
expect(exprAsts(bindings)).toEqual([null]);
|
||||
});
|
||||
|
||||
it('should allow space and colon as separators', () => {
|
||||
var bindings = parseTemplateBindings("a:b");
|
||||
expect(keys(bindings)).toEqual(['a']);
|
||||
expect(exprSources(bindings)).toEqual(['b']);
|
||||
|
||||
bindings = parseTemplateBindings("a b");
|
||||
expect(keys(bindings)).toEqual(['a']);
|
||||
expect(exprSources(bindings)).toEqual(['b']);
|
||||
});
|
||||
|
||||
it('should allow multiple pairs', () => {
|
||||
var bindings = parseTemplateBindings("a 1 b 2");
|
||||
expect(keys(bindings)).toEqual(['a', 'b']);
|
||||
expect(exprSources(bindings)).toEqual(['1 ', '2']);
|
||||
});
|
||||
|
||||
it('should store the sources in the result', () => {
|
||||
var bindings = parseTemplateBindings("a 1,b 2");
|
||||
expect(bindings[0].expression.source).toEqual('1');
|
||||
expect(bindings[1].expression.source).toEqual('2');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user