diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index e98b4141b6..fe39d133f6 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -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){} diff --git a/modules/change_detection/src/parser/lexer.js b/modules/change_detection/src/parser/lexer.js index c76310b083..a4763627b9 100644 --- a/modules/change_detection/src/parser/lexer.js +++ b/modules/change_detection/src/parser/lexer.js @@ -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([ '&', '|', '!', - '?' + '?', + '#' ]); diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index 776b87c8ed..6e4c1634cd 100644 --- a/modules/change_detection/src/parser/parser.js +++ b/modules/change_detection/src/parser/parser.js @@ -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 { + 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; diff --git a/modules/change_detection/src/watch_group.js b/modules/change_detection/src/watch_group.js index bf8c5f696e..b8832da19e 100644 --- a/modules/change_detection/src/watch_group.js +++ b/modules/change_detection/src/watch_group.js @@ -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); } diff --git a/modules/change_detection/test/change_detector_spec.js b/modules/change_detection/test/change_detector_spec.js index 656406df0b..8cadb72257 100644 --- a/modules/change_detection/test/change_detector_spec.js +++ b/modules/change_detection/test/change_detector_spec.js @@ -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) { diff --git a/modules/change_detection/test/parser/lexer_spec.js b/modules/change_detection/test/parser/lexer_spec.js index e4b5c9ef5d..025977df24 100644 --- a/modules/change_detection/test/parser/lexer_spec.js +++ b/modules/change_detection/test/parser/lexer_spec.js @@ -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 = lex("#"); + expectOperatorToken(tokens[0], 0, '#'); + }); + }); }); } diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js index b1e7102371..d5b9f6a2e2 100644 --- a/modules/change_detection/test/parser/parser_spec.js +++ b/modules/change_detection/test/parser/parser_spec.js @@ -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 { 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'); + }); + }); }); } diff --git a/modules/core/src/compiler/pipeline/compile_element.js b/modules/core/src/compiler/pipeline/compile_element.js index 4602624f6e..30fa4d971f 100644 --- a/modules/core/src/compiler/pipeline/compile_element.js +++ b/modules/core/src/compiler/pipeline/compile_element.js @@ -6,6 +6,8 @@ import {Decorator} from '../../annotations/decorator'; import {Component} from '../../annotations/component'; import {Template} from '../../annotations/template'; +import {ASTWithSource} from 'change_detection/parser/ast'; + /** * Collects all data that is needed to process an element * in the compile process. Fields are filled @@ -18,6 +20,7 @@ export class CompileElement { this._classList = null; this.textNodeBindings = null; this.propertyBindings = null; + this.variableBindings = null; this.decoratorDirectives = null; this.templateDirective = null; this.componentDirective = null; @@ -60,20 +63,27 @@ export class CompileElement { return this._classList; } - addTextNodeBinding(indexInParent:int, expression:string) { + addTextNodeBinding(indexInParent:int, expression:ASTWithSource) { if (isBlank(this.textNodeBindings)) { this.textNodeBindings = MapWrapper.create(); } MapWrapper.set(this.textNodeBindings, indexInParent, expression); } - addPropertyBinding(property:string, expression:string) { + addPropertyBinding(property:string, expression:ASTWithSource) { if (isBlank(this.propertyBindings)) { this.propertyBindings = MapWrapper.create(); } MapWrapper.set(this.propertyBindings, property, expression); } + addVariableBinding(contextName:string, templateName:string) { + if (isBlank(this.variableBindings)) { + this.variableBindings = MapWrapper.create(); + } + MapWrapper.set(this.variableBindings, contextName, templateName); + } + addDirective(directive:AnnotatedType) { var annotation = directive.annotation; if (annotation instanceof Decorator) { diff --git a/modules/core/src/compiler/pipeline/default_steps.js b/modules/core/src/compiler/pipeline/default_steps.js index 2b41949ec3..66f9f216bb 100644 --- a/modules/core/src/compiler/pipeline/default_steps.js +++ b/modules/core/src/compiler/pipeline/default_steps.js @@ -21,13 +21,13 @@ export function createDefaultSteps( directives: List ) { return [ - new PropertyBindingParser(), - new TextInterpolationParser(), + new ViewSplitter(parser), + new TextInterpolationParser(parser), + new PropertyBindingParser(parser), new DirectiveParser(directives), - new ViewSplitter(), new ElementBindingMarker(), new ProtoViewBuilder(), new ProtoElementInjectorBuilder(), - new ElementBinderBuilder(parser, closureMap) + new ElementBinderBuilder(closureMap) ]; } \ No newline at end of file diff --git a/modules/core/src/compiler/pipeline/directive_parser.js b/modules/core/src/compiler/pipeline/directive_parser.js index 75cb2efd3d..713b7a80eb 100644 --- a/modules/core/src/compiler/pipeline/directive_parser.js +++ b/modules/core/src/compiler/pipeline/directive_parser.js @@ -1,5 +1,6 @@ import {isPresent, BaseException} from 'facade/lang'; import {List, MapWrapper} from 'facade/collection'; +import {TemplateElement} from 'facade/dom'; import {SelectorMatcher} from '../selector'; import {CssSelector} from '../selector'; @@ -12,7 +13,8 @@ import {CompileControl} from './compile_control'; import {Reflector} from '../reflector'; /** - * Parses the directives on a single element. + * Parses the directives on a single element. Assumes ViewSplitter has already created + *