diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index a041b7a987..8913ef6544 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -1,4 +1,5 @@ -import {FIELD, toBool, autoConvertAdd} from "facade/lang"; +import {FIELD, toBool, autoConvertAdd, isBlank, FunctionWrapper, BaseException} from "facade/lang"; +import {List, ListWrapper} from "facade/collection"; export class AST { eval(context, formatters) { @@ -50,6 +51,28 @@ export class FieldRead extends AST { } } +export class Formatter extends AST { + constructor(exp:AST, name:string, args:List) { + this.exp = exp; + this.name = name; + this.args = args; + this.allArgs = ListWrapper.concat([exp], args); + } + + eval(context, formatters) { + var formatter = formatters[this.name]; + if (isBlank(formatter)) { + throw new BaseException(`No formatter '${this.name}' found!`); + } + var evaledArgs = evalList(context, this.allArgs, formatters); + return FunctionWrapper.apply(formatter, evaledArgs); + } + + visit(visitor) { + visitor.visitFormatter(this); + } +} + export class LiteralPrimitive extends AST { @FIELD('final value') constructor(value) { @@ -128,4 +151,15 @@ export class AstVisitor { visitBinary(ast:Binary) {} visitPrefixNot(ast:PrefixNot) {} visitLiteralPrimitive(ast:LiteralPrimitive) {} + visitFormatter(ast:Formatter) {} } + +var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]]; +function evalList(context, exps:List, formatters){ + var length = exps.length; + var result = _evalListCache[length]; + for (var i = 0; i < length; i++) { + result[i] = exps[i].eval(context, formatters); + } + return result; +} \ No newline at end of file diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index 246e0071cc..8e88081337 100644 --- a/modules/change_detection/src/parser/parser.js +++ b/modules/change_detection/src/parser/parser.js @@ -1,9 +1,18 @@ import {FIELD, int, isBlank} from 'facade/lang'; import {ListWrapper, List} from 'facade/collection'; -import {Lexer, EOF, Token, $PERIOD, $COLON} from './lexer'; +import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON} from './lexer'; import {ClosureMap} from './closure_map'; -import {AST, ImplicitReceiver, FieldRead, LiteralPrimitive, Expression, - Binary, PrefixNot, Conditional} from './ast'; +import { + AST, + ImplicitReceiver, + FieldRead, + LiteralPrimitive, + Expression, + Binary, + PrefixNot, + Conditional, + Formatter + } from './ast'; var _implicitReceiver = new ImplicitReceiver(); @@ -70,12 +79,35 @@ class _ParseAST { parseChain():AST { var exprs = []; + var isChain = false; while (this.index < this.tokens.length) { - ListWrapper.push(exprs, this.parseConditional()); + var expr = this.parseFormatter(); + ListWrapper.push(exprs, expr); + + while (this.optionalCharacter($SEMICOLON)) { + isChain = true; + } + + if (isChain && expr instanceof Formatter) { + this.error('Cannot have a formatter in a chain'); + } } return ListWrapper.first(exprs); } + parseFormatter() { + var result = this.parseExpression(); + while (this.optionalOperator("|")) { + var name = this.parseIdentifier(); + var args = ListWrapper.create(); + while (this.optionalCharacter($COLON)) { + ListWrapper.push(args, this.parseExpression()); + } + result = new Formatter(result, name, args); + } + return result; + } + parseExpression() { return this.parseConditional(); } diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js index b55acf4efa..9ad329b8d9 100644 --- a/modules/change_detection/test/parser/parser_spec.js +++ b/modules/change_detection/test/parser/parser_spec.js @@ -1,4 +1,5 @@ import {ddescribe, describe, it, iit, expect, beforeEach} from 'test_lib/test_lib'; +import {BaseException} from 'facade/lang'; import {Parser} from 'change_detection/parser/parser'; import {Lexer} from 'change_detection/parser/lexer'; import {ClosureMap} from 'change_detection/parser/closure_map'; @@ -12,7 +13,7 @@ class TestData { class ContextWithErrors { get boo() { - throw new Error("boo to you"); + throw new BaseException("boo to you"); } } @@ -146,10 +147,41 @@ export function main() { expectEval("null - null").toBeNull(); }); }); + + describe('formatters', () => { + beforeEach(() => { + formatters = { + "uppercase": (s) => s.toUpperCase(), + "lowercase": (s) => s.toLowerCase(), + "increment": (a,b) => a + b + } + }); + + it('should call a formatter', () => { + expectEval("'Foo'|uppercase").toEqual("FOO"); + expectEval("'fOo'|uppercase|lowercase").toEqual("foo"); + }); + + it('should call a formatter with arguments', () => { + expectEval("1|increment:2").toEqual(3); + }); + + it('should throw when invalid formatter', () => { + expectEvalError("1|nonexistent").toThrowError('No formatter \'nonexistent\' found!'); + });; + + it('should not allow formatters in a chain', () => { + expectEvalError("1;'World'|hello"). + toThrowError(new RegExp('Cannot have a formatter in a chain')); + expectEvalError("'World'|hello;1"). + toThrowError(new RegExp('Cannot have a formatter in a chain')); + }); + }); describe("error handling", () => { it('should throw on incorrect ternary operator syntax', () => { - expectEvalError("true?1").toThrowError(new RegExp('Parser Error: Conditional expression true\\?1 requires all 3 expressions')); + expectEvalError("true?1"). + toThrowError(new RegExp('Parser Error: Conditional expression true\\?1 requires all 3 expressions')); }); it('should pass exceptions', () => { diff --git a/modules/facade/src/collection.dart b/modules/facade/src/collection.dart index 6e5aac3754..0b86d72db8 100644 --- a/modules/facade/src/collection.dart +++ b/modules/facade/src/collection.dart @@ -46,6 +46,7 @@ class ListWrapper { static last(List list) => list.last; static List reversed(List list) => list.reversed.toList(); static void push(List l, e) { l.add(e); } + static List concat(List a, List b) {a.addAll(b); return a;} } class SetWrapper { diff --git a/modules/facade/src/collection.es6 b/modules/facade/src/collection.es6 index a41c9bdc77..cf6ebda1f0 100644 --- a/modules/facade/src/collection.es6 +++ b/modules/facade/src/collection.es6 @@ -79,6 +79,7 @@ export class ListWrapper { var a = ListWrapper.clone(array); return a.reverse(); } + static concat(a, b) {return a.concat(b);} } export class SetWrapper { diff --git a/modules/facade/src/lang.dart b/modules/facade/src/lang.dart index a14abaf655..b9a6e71682 100644 --- a/modules/facade/src/lang.dart +++ b/modules/facade/src/lang.dart @@ -103,3 +103,18 @@ class RegExpMatcherWrapper { } } +class FunctionWrapper { + static apply(Function fn, posArgs) { + return Function.apply(fn, posArgs); + } +} + +class BaseException extends Error { + final String message; + + BaseException(this.message); + + String toString() { + return this.message; + } +} \ No newline at end of file diff --git a/modules/facade/src/lang.es6 b/modules/facade/src/lang.es6 index baca880c8b..8a4a75f06c 100644 --- a/modules/facade/src/lang.es6 +++ b/modules/facade/src/lang.es6 @@ -137,3 +137,18 @@ export class RegExpMatcherWrapper { } } +export class FunctionWrapper { + static apply(fn:Function, posArgs) { + return fn.apply(null, posArgs); + } +} + +export class BaseException extends Error { + constructor(message){ + this.message = message; + } + + toString():String { + return this.message; + } +} \ No newline at end of file