Skip to content

Commit 00bc9e5

Browse files
committed
feat(parser): add support for formatters
1 parent 8a829d3 commit 00bc9e5

File tree

7 files changed

+137
-7
lines changed

7 files changed

+137
-7
lines changed

modules/change_detection/src/parser/ast.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {FIELD, toBool, autoConvertAdd} from "facade/lang";
1+
import {FIELD, toBool, autoConvertAdd, isBlank, FunctionWrapper, BaseException} from "facade/lang";
2+
import {List, ListWrapper} from "facade/collection";
23

34
export class AST {
45
eval(context, formatters) {
@@ -50,6 +51,28 @@ export class FieldRead extends AST {
5051
}
5152
}
5253

54+
export class Formatter extends AST {
55+
constructor(exp:AST, name:string, args:List) {
56+
this.exp = exp;
57+
this.name = name;
58+
this.args = args;
59+
this.allArgs = ListWrapper.concat([exp], args);
60+
}
61+
62+
eval(context, formatters) {
63+
var formatter = formatters[this.name];
64+
if (isBlank(formatter)) {
65+
throw new BaseException(`No formatter '${this.name}' found!`);
66+
}
67+
var evaledArgs = evalList(context, this.allArgs, formatters);
68+
return FunctionWrapper.apply(formatter, evaledArgs);
69+
}
70+
71+
visit(visitor) {
72+
visitor.visitFormatter(this);
73+
}
74+
}
75+
5376
export class LiteralPrimitive extends AST {
5477
@FIELD('final value')
5578
constructor(value) {
@@ -128,4 +151,15 @@ export class AstVisitor {
128151
visitBinary(ast:Binary) {}
129152
visitPrefixNot(ast:PrefixNot) {}
130153
visitLiteralPrimitive(ast:LiteralPrimitive) {}
154+
visitFormatter(ast:Formatter) {}
131155
}
156+
157+
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];
158+
function evalList(context, exps:List, formatters){
159+
var length = exps.length;
160+
var result = _evalListCache[length];
161+
for (var i = 0; i < length; i++) {
162+
result[i] = exps[i].eval(context, formatters);
163+
}
164+
return result;
165+
}

modules/change_detection/src/parser/parser.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import {FIELD, int, isBlank} from 'facade/lang';
22
import {ListWrapper, List} from 'facade/collection';
3-
import {Lexer, EOF, Token, $PERIOD, $COLON} from './lexer';
3+
import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON} from './lexer';
44
import {ClosureMap} from './closure_map';
5-
import {AST, ImplicitReceiver, FieldRead, LiteralPrimitive, Expression,
6-
Binary, PrefixNot, Conditional} from './ast';
5+
import {
6+
AST,
7+
ImplicitReceiver,
8+
FieldRead,
9+
LiteralPrimitive,
10+
Expression,
11+
Binary,
12+
PrefixNot,
13+
Conditional,
14+
Formatter
15+
} from './ast';
716

817
var _implicitReceiver = new ImplicitReceiver();
918

@@ -70,12 +79,35 @@ class _ParseAST {
7079

7180
parseChain():AST {
7281
var exprs = [];
82+
var isChain = false;
7383
while (this.index < this.tokens.length) {
74-
ListWrapper.push(exprs, this.parseConditional());
84+
var expr = this.parseFormatter();
85+
ListWrapper.push(exprs, expr);
86+
87+
while (this.optionalCharacter($SEMICOLON)) {
88+
isChain = true;
89+
}
90+
91+
if (isChain && expr instanceof Formatter) {
92+
this.error('Cannot have a formatter in a chain');
93+
}
7594
}
7695
return ListWrapper.first(exprs);
7796
}
7897

98+
parseFormatter() {
99+
var result = this.parseExpression();
100+
while (this.optionalOperator("|")) {
101+
var name = this.parseIdentifier();
102+
var args = ListWrapper.create();
103+
while (this.optionalCharacter($COLON)) {
104+
ListWrapper.push(args, this.parseExpression());
105+
}
106+
result = new Formatter(result, name, args);
107+
}
108+
return result;
109+
}
110+
79111
parseExpression() {
80112
return this.parseConditional();
81113
}

modules/change_detection/test/parser/parser_spec.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {ddescribe, describe, it, iit, expect, beforeEach} from 'test_lib/test_lib';
2+
import {BaseException} from 'facade/lang';
23
import {Parser} from 'change_detection/parser/parser';
34
import {Lexer} from 'change_detection/parser/lexer';
45
import {ClosureMap} from 'change_detection/parser/closure_map';
@@ -12,7 +13,7 @@ class TestData {
1213

1314
class ContextWithErrors {
1415
get boo() {
15-
throw new Error("boo to you");
16+
throw new BaseException("boo to you");
1617
}
1718
}
1819

@@ -146,10 +147,41 @@ export function main() {
146147
expectEval("null - null").toBeNull();
147148
});
148149
});
150+
151+
describe('formatters', () => {
152+
beforeEach(() => {
153+
formatters = {
154+
"uppercase": (s) => s.toUpperCase(),
155+
"lowercase": (s) => s.toLowerCase(),
156+
"increment": (a,b) => a + b
157+
}
158+
});
159+
160+
it('should call a formatter', () => {
161+
expectEval("'Foo'|uppercase").toEqual("FOO");
162+
expectEval("'fOo'|uppercase|lowercase").toEqual("foo");
163+
});
164+
165+
it('should call a formatter with arguments', () => {
166+
expectEval("1|increment:2").toEqual(3);
167+
});
168+
169+
it('should throw when invalid formatter', () => {
170+
expectEvalError("1|nonexistent").toThrowError('No formatter \'nonexistent\' found!');
171+
});;
172+
173+
it('should not allow formatters in a chain', () => {
174+
expectEvalError("1;'World'|hello").
175+
toThrowError(new RegExp('Cannot have a formatter in a chain'));
176+
expectEvalError("'World'|hello;1").
177+
toThrowError(new RegExp('Cannot have a formatter in a chain'));
178+
});
179+
});
149180

150181
describe("error handling", () => {
151182
it('should throw on incorrect ternary operator syntax', () => {
152-
expectEvalError("true?1").toThrowError(new RegExp('Parser Error: Conditional expression true\\?1 requires all 3 expressions'));
183+
expectEvalError("true?1").
184+
toThrowError(new RegExp('Parser Error: Conditional expression true\\?1 requires all 3 expressions'));
153185
});
154186

155187
it('should pass exceptions', () => {

modules/facade/src/collection.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class ListWrapper {
4646
static last(List list) => list.last;
4747
static List reversed(List list) => list.reversed.toList();
4848
static void push(List l, e) { l.add(e); }
49+
static List concat(List a, List b) {a.addAll(b); return a;}
4950
}
5051

5152
class SetWrapper {

modules/facade/src/collection.es6

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class ListWrapper {
7979
var a = ListWrapper.clone(array);
8080
return a.reverse();
8181
}
82+
static concat(a, b) {return a.concat(b);}
8283
}
8384

8485
export class SetWrapper {

modules/facade/src/lang.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,18 @@ class RegExpMatcherWrapper {
103103
}
104104
}
105105

106+
class FunctionWrapper {
107+
static apply(Function fn, posArgs) {
108+
return Function.apply(fn, posArgs);
109+
}
110+
}
111+
112+
class BaseException extends Error {
113+
final String message;
114+
115+
BaseException(this.message);
116+
117+
String toString() {
118+
return this.message;
119+
}
120+
}

modules/facade/src/lang.es6

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,18 @@ export class RegExpMatcherWrapper {
137137
}
138138
}
139139

140+
export class FunctionWrapper {
141+
static apply(fn:Function, posArgs) {
142+
return fn.apply(null, posArgs);
143+
}
144+
}
145+
146+
export class BaseException extends Error {
147+
constructor(message){
148+
this.message = message;
149+
}
150+
151+
toString():String {
152+
return this.message;
153+
}
154+
}

0 commit comments

Comments
 (0)