From 3b34ef43b185609b307199e53370612b0e12f7b4 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 8 Jan 2015 16:17:56 -0800 Subject: [PATCH] perf(CD): Special cased interpolation in AST, Parser, and CD --- modules/change_detection/src/parser/ast.js | 17 ++++++++ modules/change_detection/src/parser/parser.js | 29 +++++++++++++- .../src/proto_change_detector.js | 39 ++++++++++++++++++- .../test/parser/parser_spec.js | 25 ++++++++++++ .../pipeline/property_binding_parser.js | 12 +++--- .../pipeline/text_interpolation_parser.js | 38 ++---------------- .../pipeline/property_binding_parser_spec.js | 4 +- .../text_interpolation_parser_spec.js | 16 ++++---- 8 files changed, 128 insertions(+), 52 deletions(-) diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index 2c77895941..1e01b41fa5 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -249,6 +249,23 @@ export class LiteralMap extends AST { } } +export class Interpolation extends AST { + strings:List; + expressions:List; + constructor(strings:List, expressions:List) { + this.strings = strings; + this.expressions = expressions; + } + + eval(context) { + throw new Error("unsuported"); + } + + visit(visitor, args) { + visitor.visitInterpolation(this, args); + } +} + export class Binary extends AST { operation:string; left:AST; diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index 8795156911..d0423429bd 100644 --- a/modules/change_detection/src/parser/parser.js +++ b/modules/change_detection/src/parser/parser.js @@ -1,4 +1,4 @@ -import {FIELD, int, isBlank, isPresent, BaseException, StringWrapper} from 'facade/lang'; +import {FIELD, int, isBlank, isPresent, BaseException, StringWrapper, RegExpWrapper} from 'facade/lang'; import {ListWrapper, List} from 'facade/collection'; import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON, $LBRACKET, $RBRACKET, $COMMA, $LBRACE, $RBRACE, $LPAREN, $RPAREN} from './lexer'; @@ -19,6 +19,7 @@ import { KeyedAccess, LiteralArray, LiteralMap, + Interpolation, MethodCall, FunctionCall, TemplateBindings, @@ -27,6 +28,9 @@ import { } from './ast'; var _implicitReceiver = new ImplicitReceiver(); +// TODO(tbosch): Cannot make this const/final right now because of the transpiler... +var INTERPOLATION_REGEXP = RegExpWrapper.create('\\{\\{(.*?)\\}\\}'); +var QUOTE_REGEXP = RegExpWrapper.create("'"); export class Parser { _lexer:Lexer; @@ -52,6 +56,29 @@ export class Parser { var tokens = this._lexer.tokenize(input); return new _ParseAST(input, location, tokens, this._reflector, false).parseTemplateBindings(); } + + parseInterpolation(input:string, location:any):ASTWithSource { + var parts = StringWrapper.split(input, INTERPOLATION_REGEXP); + if (parts.length <= 1) { + return null; + } + var strings = []; + var expressions = []; + + for (var i=0; i ++i ? strings[i] : null; + var c1 = length > ++i ? strings[i] : null; + var c2 = length > ++i ? strings[i] : null; + var c3 = length > ++i ? strings[i] : null; + var c4 = length > ++i ? strings[i] : null; + var c5 = length > ++i ? strings[i] : null; + var c6 = length > ++i ? strings[i] : null; + var c7 = length > ++i ? strings[i] : null; + var c8 = length > ++i ? strings[i] : null; + var c9 = length > ++i ? strings[i] : null; + switch (length - 1) { + case 1: return (a1) => c0 + s(a1) + c1; + case 2: return (a1, a2) => c0 + s(a1) + c1 + s(a2) + c2; + case 3: return (a1, a2, a3) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3; + case 4: return (a1, a2, a3, a4) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4; + case 5: return (a1, a2, a3, a4, a5) => c0 + s(a1) + c1 + s(a2) + c2 + s(a3) + c3 + s(a4) + c4 + s(a5) + c5; + 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; + 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; + 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; + 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; + default: throw new BaseException(`Does not support more than 9 expressions`); + } +} diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js index 1cc4c64f61..efc5c3ada2 100644 --- a/modules/change_detection/test/parser/parser_spec.js +++ b/modules/change_detection/test/parser/parser_spec.js @@ -47,6 +47,10 @@ export function main() { return createParser().parseTemplateBindings(text, location); } + function parseInterpolation(text, location = null) { + return createParser().parseInterpolation(text, location); + } + function expectEval(text, passedInContext = null) { var c = isBlank(passedInContext) ? td() : passedInContext; return expect(parseAction(text).eval(c)); @@ -494,6 +498,27 @@ export function main() { expect(bindings[0].expression.location).toEqual('location'); }); }); + + describe('parseInterpolation', () => { + it('should return null if no interpolation', () => { + expect(parseInterpolation('nothing')).toBe(null); + }); + + it('should parse no prefix/suffix interpolation', () => { + var ast = parseInterpolation('{{a}}').ast; + expect(ast.strings).toEqual(['', '']); + expect(ast.expressions.length).toEqual(1); + expect(ast.expressions[0].name).toEqual('a'); + }); + + it('should parse prefix/suffix with multiple interpolation', () => { + var ast = parseInterpolation('before{{a}}middle{{b}}after').ast; + expect(ast.strings).toEqual(['before', 'middle', 'after']); + expect(ast.expressions.length).toEqual(2); + expect(ast.expressions[0].name).toEqual('a'); + expect(ast.expressions[1].name).toEqual('b'); + }); + }); }); } diff --git a/modules/core/src/compiler/pipeline/property_binding_parser.js b/modules/core/src/compiler/pipeline/property_binding_parser.js index 3c1626c39e..ebece54e36 100644 --- a/modules/core/src/compiler/pipeline/property_binding_parser.js +++ b/modules/core/src/compiler/pipeline/property_binding_parser.js @@ -8,8 +8,6 @@ import {CompileStep} from './compile_step'; import {CompileElement} from './compile_element'; import {CompileControl} from './compile_control'; -import {interpolationToExpression} from './text_interpolation_parser'; - // TODO(tbosch): Cannot make this const/final right now because of the transpiler... var BIND_NAME_REGEXP = RegExpWrapper.create('^(?:(?:(bind)|(let)|(on))-(.+))|\\[([^\\]]+)\\]|\\(([^\\)]+)\\)'); @@ -56,14 +54,18 @@ export class PropertyBindingParser extends CompileStep { current.addEventBinding(bindParts[6], this._parseBinding(attrValue)); } } else { - var expression = interpolationToExpression(attrValue); - if (isPresent(expression)) { - current.addPropertyBinding(attrName, this._parseBinding(expression)); + var ast = this._parseInterpolation(attrValue); + if (isPresent(ast)) { + current.addPropertyBinding(attrName, ast); } } }); } + _parseInterpolation(input:string):AST { + return this._parser.parseInterpolation(input, this._compilationUnit); + } + _parseBinding(input:string):AST { return this._parser.parseBinding(input, this._compilationUnit); } diff --git a/modules/core/src/compiler/pipeline/text_interpolation_parser.js b/modules/core/src/compiler/pipeline/text_interpolation_parser.js index 6c89c136cb..3d91cc398e 100644 --- a/modules/core/src/compiler/pipeline/text_interpolation_parser.js +++ b/modules/core/src/compiler/pipeline/text_interpolation_parser.js @@ -7,38 +7,6 @@ import {CompileStep} from './compile_step'; import {CompileElement} from './compile_element'; import {CompileControl} from './compile_control'; -// TODO(tbosch): Cannot make this const/final right now because of the transpiler... -var INTERPOLATION_REGEXP = RegExpWrapper.create('\\{\\{(.*?)\\}\\}'); -var QUOTE_REGEXP = RegExpWrapper.create("'"); - -export function interpolationToExpression(value:string):string { - // TODO: add stringify formatter when we support formatters - var parts = StringWrapper.split(value, INTERPOLATION_REGEXP); - if (parts.length <= 1) { - return null; - } - var expression = ''; - for (var i=0; i 0) { - expressionPart = "'" + StringWrapper.replaceAll(parts[i], QUOTE_REGEXP, "\\'") + "'"; - } - } else { - // expression - expressionPart = "(" + parts[i] + ")"; - } - if (isPresent(expressionPart)) { - if (expression.length > 0) { - expression += '+'; - } - expression += expressionPart; - } - } - return expression; -} - /** * Parses interpolations in direct text child nodes of the current element. * @@ -68,10 +36,10 @@ export class TextInterpolationParser extends CompileStep { } _parseTextNode(pipelineElement, node, nodeIndex) { - var expression = interpolationToExpression(node.nodeValue); - if (isPresent(expression)) { + var ast = this._parser.parseInterpolation(node.nodeValue, this._compilationUnit); + if (isPresent(ast)) { DOM.setText(node, ' '); - pipelineElement.addTextNodeBinding(nodeIndex, this._parser.parseBinding(expression, this._compilationUnit)); + pipelineElement.addTextNodeBinding(nodeIndex, ast); } } } diff --git a/modules/core/test/compiler/pipeline/property_binding_parser_spec.js b/modules/core/test/compiler/pipeline/property_binding_parser_spec.js index d617d9bdac..dd80504cb2 100644 --- a/modules/core/test/compiler/pipeline/property_binding_parser_spec.js +++ b/modules/core/test/compiler/pipeline/property_binding_parser_spec.js @@ -26,7 +26,7 @@ export function main() { // Note: we don't test all corner cases of interpolation as we assume shared functionality between text interpolation // and attribute interpolation. var results = createPipeline().process(el('
')); - expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('(b)'); + expect(MapWrapper.get(results[0].propertyBindings, 'a').source).toEqual('{{b}}'); }); it('should detect let- syntax', () => { @@ -54,4 +54,4 @@ export function main() { expect(MapWrapper.get(results[0].eventBindings, 'click').source).toEqual('b()'); }); }); -} \ No newline at end of file +} diff --git a/modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js b/modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js index c83533a0b8..94ce36768c 100644 --- a/modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js +++ b/modules/core/test/compiler/pipeline/text_interpolation_parser_spec.js @@ -19,39 +19,39 @@ export function main() { it('should find text interpolation in normal elements', () => { var results = createPipeline().process(el('
{{expr1}}{{expr2}}
')); var bindings = results[0].textNodeBindings; - expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)"); - expect(MapWrapper.get(bindings, 2).source).toEqual("(expr2)"); + expect(MapWrapper.get(bindings, 0).source).toEqual("{{expr1}}"); + expect(MapWrapper.get(bindings, 2).source).toEqual("{{expr2}}"); }); it('should find text interpolation in template elements', () => { var results = createPipeline().process(el('')); var bindings = results[0].textNodeBindings; - expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)"); - expect(MapWrapper.get(bindings, 2).source).toEqual("(expr2)"); + expect(MapWrapper.get(bindings, 0).source).toEqual("{{expr1}}"); + expect(MapWrapper.get(bindings, 2).source).toEqual("{{expr2}}"); }); it('should allow multiple expressions', () => { var results = createPipeline().process(el('
{{expr1}}{{expr2}}
')); var bindings = results[0].textNodeBindings; - expect(MapWrapper.get(bindings, 0).source).toEqual("(expr1)+(expr2)"); + expect(MapWrapper.get(bindings, 0).source).toEqual("{{expr1}}{{expr2}}"); }); it('should not interpolate when compileChildren is false', () => { var results = createPipeline().process(el('
{{included}}{{excluded}}
')); var bindings = results[0].textNodeBindings; - expect(MapWrapper.get(bindings, 0).source).toEqual("(included)"); + expect(MapWrapper.get(bindings, 0).source).toEqual("{{included}}"); expect(results[1].textNodeBindings).toBe(null); }); it('should allow fixed text before, in between and after expressions', () => { var results = createPipeline().process(el('
a{{expr1}}b{{expr2}}c
')); var bindings = results[0].textNodeBindings; - expect(MapWrapper.get(bindings, 0).source).toEqual("'a'+(expr1)+'b'+(expr2)+'c'"); + expect(MapWrapper.get(bindings, 0).source).toEqual("a{{expr1}}b{{expr2}}c"); }); it('should escape quotes in fixed parts', () => { var results = createPipeline().process(el("
'\"a{{expr1}}
")); - expect(MapWrapper.get(results[0].textNodeBindings, 0).source).toEqual("'\\'\"a'+(expr1)"); + expect(MapWrapper.get(results[0].textNodeBindings, 0).source).toEqual("'\"a{{expr1}}"); }); }); }