Skip to content

Commit 3b34ef4

Browse files
committed
perf(CD): Special cased interpolation in AST, Parser, and CD
1 parent ee99a5a commit 3b34ef4

File tree

8 files changed

+128
-52
lines changed

8 files changed

+128
-52
lines changed

modules/change_detection/src/parser/ast.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ export class LiteralMap extends AST {
249249
}
250250
}
251251

252+
export class Interpolation extends AST {
253+
strings:List;
254+
expressions:List;
255+
constructor(strings:List, expressions:List) {
256+
this.strings = strings;
257+
this.expressions = expressions;
258+
}
259+
260+
eval(context) {
261+
throw new Error("unsuported");
262+
}
263+
264+
visit(visitor, args) {
265+
visitor.visitInterpolation(this, args);
266+
}
267+
}
268+
252269
export class Binary extends AST {
253270
operation:string;
254271
left:AST;

modules/change_detection/src/parser/parser.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {FIELD, int, isBlank, isPresent, BaseException, StringWrapper} from 'facade/lang';
1+
import {FIELD, int, isBlank, isPresent, BaseException, StringWrapper, RegExpWrapper} from 'facade/lang';
22
import {ListWrapper, List} from 'facade/collection';
33
import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON, $LBRACKET, $RBRACKET,
44
$COMMA, $LBRACE, $RBRACE, $LPAREN, $RPAREN} from './lexer';
@@ -19,6 +19,7 @@ import {
1919
KeyedAccess,
2020
LiteralArray,
2121
LiteralMap,
22+
Interpolation,
2223
MethodCall,
2324
FunctionCall,
2425
TemplateBindings,
@@ -27,6 +28,9 @@ import {
2728
} from './ast';
2829

2930
var _implicitReceiver = new ImplicitReceiver();
31+
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
32+
var INTERPOLATION_REGEXP = RegExpWrapper.create('\\{\\{(.*?)\\}\\}');
33+
var QUOTE_REGEXP = RegExpWrapper.create("'");
3034

3135
export class Parser {
3236
_lexer:Lexer;
@@ -52,6 +56,29 @@ export class Parser {
5256
var tokens = this._lexer.tokenize(input);
5357
return new _ParseAST(input, location, tokens, this._reflector, false).parseTemplateBindings();
5458
}
59+
60+
parseInterpolation(input:string, location:any):ASTWithSource {
61+
var parts = StringWrapper.split(input, INTERPOLATION_REGEXP);
62+
if (parts.length <= 1) {
63+
return null;
64+
}
65+
var strings = [];
66+
var expressions = [];
67+
68+
for (var i=0; i<parts.length; i++) {
69+
var part = parts[i];
70+
if (i%2 === 0) {
71+
// fixed string
72+
ListWrapper.push(strings, part);
73+
} else {
74+
var tokens = this._lexer.tokenize(part);
75+
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
76+
ListWrapper.push(expressions, ast);
77+
}
78+
}
79+
return new ASTWithSource(new Interpolation(strings, expressions), input, location);
80+
}
81+
5582
}
5683

5784
class _ParseAST {

modules/change_detection/src/proto_change_detector.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Formatter,
1515
FunctionCall,
1616
ImplicitReceiver,
17+
Interpolation,
1718
KeyedAccess,
1819
LiteralArray,
1920
LiteralMap,
@@ -116,6 +117,11 @@ class ProtoOperationsCreator {
116117
return 0;
117118
}
118119

120+
visitInterpolation(ast:Interpolation) {
121+
var args = this._visitAll(ast.expressions);
122+
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Interpolate()", _interpolationFn(ast.strings), args, 0);
123+
}
124+
119125
visitLiteralPrimitive(ast:LiteralPrimitive) {
120126
return this._addRecord(RECORD_TYPE_CONST, null, ast.value, [], 0);
121127
}
@@ -274,4 +280,35 @@ function _cond(cond, trueVal, falseVal) {return cond ? trueVal :
274280

275281
function _keyedAccess(obj, args) {
276282
return obj[args[0]];
277-
}
283+
}
284+
285+
function s(v) {
286+
return isPresent(v) ? '' + v : '';
287+
}
288+
289+
function _interpolationFn(strings:List) {
290+
var length = strings.length;
291+
var i = -1;
292+
var c0 = length > ++i ? strings[i] : null;
293+
var c1 = length > ++i ? strings[i] : null;
294+
var c2 = length > ++i ? strings[i] : null;
295+
var c3 = length > ++i ? strings[i] : null;
296+
var c4 = length > ++i ? strings[i] : null;
297+
var c5 = length > ++i ? strings[i] : null;
298+
var c6 = length > ++i ? strings[i] : null;
299+
var c7 = length > ++i ? strings[i] : null;
300+
var c8 = length > ++i ? strings[i] : null;
301+
var c9 = length > ++i ? strings[i] : null;
302+
switch (length - 1) {
303+
case 1: return (a1) => c0 + s(a1) + c1;
304+
case 2: return (a1, a2) => c0 + s(a1) + c1 + s(a2) + c2;
305+
case 3: return (a1, a2, a3) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3;
306+
case 4: return (a1, a2, a3, a4) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4;
307+
case 5: return (a1, a2, a3, a4, a5) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5;
308+
case 6: return (a1, a2, a3, a4, a5, a6) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6;
309+
case 7: return (a1, a2, a3, a4, a5, a6, a7) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6 + s(a7) + c7;
310+
case 8: return (a1, a2, a3, a4, a5, a6, a7, a8) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6 + s(a7) + c7 + s(a8) + c8;
311+
case 9: return (a1, a2, a3, a4, a5, a6, a7, a8, a9) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5 + s(a6) + c6 + s(a7) + c7 + s(a8) + c8 + s(a9) + c9;
312+
default: throw new BaseException(`Does not support more than 9 expressions`);
313+
}
314+
}

modules/change_detection/test/parser/parser_spec.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export function main() {
4747
return createParser().parseTemplateBindings(text, location);
4848
}
4949

50+
function parseInterpolation(text, location = null) {
51+
return createParser().parseInterpolation(text, location);
52+
}
53+
5054
function expectEval(text, passedInContext = null) {
5155
var c = isBlank(passedInContext) ? td() : passedInContext;
5256
return expect(parseAction(text).eval(c));
@@ -494,6 +498,27 @@ export function main() {
494498
expect(bindings[0].expression.location).toEqual('location');
495499
});
496500
});
501+
502+
describe('parseInterpolation', () => {
503+
it('should return null if no interpolation', () => {
504+
expect(parseInterpolation('nothing')).toBe(null);
505+
});
506+
507+
it('should parse no prefix/suffix interpolation', () => {
508+
var ast = parseInterpolation('{{a}}').ast;
509+
expect(ast.strings).toEqual(['', '']);
510+
expect(ast.expressions.length).toEqual(1);
511+
expect(ast.expressions[0].name).toEqual('a');
512+
});
513+
514+
it('should parse prefix/suffix with multiple interpolation', () => {
515+
var ast = parseInterpolation('before{{a}}middle{{b}}after').ast;
516+
expect(ast.strings).toEqual(['before', 'middle', 'after']);
517+
expect(ast.expressions.length).toEqual(2);
518+
expect(ast.expressions[0].name).toEqual('a');
519+
expect(ast.expressions[1].name).toEqual('b');
520+
});
521+
});
497522
});
498523
}
499524

modules/core/src/compiler/pipeline/property_binding_parser.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import {CompileStep} from './compile_step';
88
import {CompileElement} from './compile_element';
99
import {CompileControl} from './compile_control';
1010

11-
import {interpolationToExpression} from './text_interpolation_parser';
12-
1311
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
1412
var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(let)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\)]+)\\)');
1513

@@ -56,14 +54,18 @@ export class PropertyBindingParser extends CompileStep {
5654
current.addEventBinding(bindParts[6], this._parseBinding(attrValue));
5755
}
5856
} else {
59-
var expression = interpolationToExpression(attrValue);
60-
if (isPresent(expression)) {
61-
current.addPropertyBinding(attrName, this._parseBinding(expression));
57+
var ast = this._parseInterpolation(attrValue);
58+
if (isPresent(ast)) {
59+
current.addPropertyBinding(attrName, ast);
6260
}
6361
}
6462
});
6563
}
6664

65+
_parseInterpolation(input:string):AST {
66+
return this._parser.parseInterpolation(input, this._compilationUnit);
67+
}
68+
6769
_parseBinding(input:string):AST {
6870
return this._parser.parseBinding(input, this._compilationUnit);
6971
}

modules/core/src/compiler/pipeline/text_interpolation_parser.js

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,6 @@ import {CompileStep} from './compile_step';
77
import {CompileElement} from './compile_element';
88
import {CompileControl} from './compile_control';
99

10-
// TODO(tbosch): Cannot make this const/final right now because of the transpiler...
11-
var INTERPOLATION_REGEXP = RegExpWrapper.create('\\{\\{(.*?)\\}\\}');
12-
var QUOTE_REGEXP = RegExpWrapper.create("'");
13-
14-
export function interpolationToExpression(value:string):string {
15-
// TODO: add stringify formatter when we support formatters
16-
var parts = StringWrapper.split(value, INTERPOLATION_REGEXP);
17-
if (parts.length <= 1) {
18-
return null;
19-
}
20-
var expression = '';
21-
for (var i=0; i<parts.length; i++) {
22-
var expressionPart = null;
23-
if (i%2 === 0) {
24-
// fixed string
25-
if (parts[i].length > 0) {
26-
expressionPart = "'" + StringWrapper.replaceAll(parts[i], QUOTE_REGEXP, "\\'") + "'";
27-
}
28-
} else {
29-
// expression
30-
expressionPart = "(" + parts[i] + ")";
31-
}
32-
if (isPresent(expressionPart)) {
33-
if (expression.length > 0) {
34-
expression += '+';
35-
}
36-
expression += expressionPart;
37-
}
38-
}
39-
return expression;
40-
}
41-
4210
/**
4311
* Parses interpolations in direct text child nodes of the current element.
4412
*
@@ -68,10 +36,10 @@ export class TextInterpolationParser extends CompileStep {
6836
}
6937

7038
_parseTextNode(pipelineElement, node, nodeIndex) {
71-
var expression = interpolationToExpression(node.nodeValue);
72-
if (isPresent(expression)) {
39+
var ast = this._parser.parseInterpolation(node.nodeValue, this._compilationUnit);
40+
if (isPresent(ast)) {
7341
DOM.setText(node, ' ');
74-
pipelineElement.addTextNodeBinding(nodeIndex, this._parser.parseBinding(expression, this._compilationUnit));
42+
pipelineElement.addTextNodeBinding(nodeIndex, ast);
7543
}
7644
}
7745
}

modules/core/test/compiler/pipeline/property_binding_parser_spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function main() {
2626
// Note: we don't test all corner cases of interpolation as we assume shared functionality between text interpolation
2727
// and attribute interpolation.
2828
var results = createPipeline().process(el('<div a="{{b}}"></div>'));
29-
expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('(b)');
29+
expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('{{b}}');
3030
});
3131

3232
it('should detect let- syntax', () => {
@@ -54,4 +54,4 @@ export function main() {
5454
expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()');
5555
});
5656
});
57-
}
57+
}

modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,39 @@ export function main() {
1919
it('should find text interpolation in normal elements', () => {
2020
var results = createPipeline().process(el('<div>{{expr1}}<span></span>{{expr2}}</div>'));
2121
var bindings = results[0].textNodeBindings;
22-
expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)");
23-
expect(MapWrapper.get(bindings, 2).source).toEqual("(expr2)");
22+
expect(MapWrapper.get(bindings, 0).source).toEqual("{{expr1}}");
23+
expect(MapWrapper.get(bindings, 2).source).toEqual("{{expr2}}");
2424
});
2525

2626
it('should find text interpolation in template elements', () => {
2727
var results = createPipeline().process(el('<template>{{expr1}}<span></span>{{expr2}}</template>'));
2828
var bindings = results[0].textNodeBindings;
29-
expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)");
30-
expect(MapWrapper.get(bindings, 2).source).toEqual("(expr2)");
29+
expect(MapWrapper.get(bindings, 0).source).toEqual("{{expr1}}");
30+
expect(MapWrapper.get(bindings, 2).source).toEqual("{{expr2}}");
3131
});
3232

3333
it('should allow multiple expressions', () => {
3434
var results = createPipeline().process(el('<div>{{expr1}}{{expr2}}</div>'));
3535
var bindings = results[0].textNodeBindings;
36-
expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)+(expr2)");
36+
expect(MapWrapper.get(bindings, 0).source).toEqual("{{expr1}}{{expr2}}");
3737
});
3838

3939
it('should not interpolate when compileChildren is false', () => {
4040
var results = createPipeline().process(el('<div>{{included}}<span ignore-children>{{excluded}}</span></div>'));
4141
var bindings = results[0].textNodeBindings;
42-
expect(MapWrapper.get(bindings, 0).source).toEqual("(included)");
42+
expect(MapWrapper.get(bindings, 0).source).toEqual("{{included}}");
4343
expect(results[1].textNodeBindings).toBe(null);
4444
});
4545

4646
it('should allow fixed text before, in between and after expressions', () => {
4747
var results = createPipeline().process(el('<div>a{{expr1}}b{{expr2}}c</div>'));
4848
var bindings = results[0].textNodeBindings;
49-
expect(MapWrapper.get(bindings, 0).source).toEqual("'a'+(expr1)+'b'+(expr2)+'c'");
49+
expect(MapWrapper.get(bindings, 0).source).toEqual("a{{expr1}}b{{expr2}}c");
5050
});
5151

5252
it('should escape quotes in fixed parts', () => {
5353
var results = createPipeline().process(el("<div>'\"a{{expr1}}</div>"));
54-
expect(MapWrapper.get(results[0].textNodeBindings, 0).source).toEqual("'\\'\"a'+(expr1)");
54+
expect(MapWrapper.get(results[0].textNodeBindings, 0).source).toEqual("'\"a{{expr1}}");
5555
});
5656
});
5757
}

0 commit comments

Comments
 (0)