perf(CD): Special cased interpolation in AST, Parser, and CD

This commit is contained in:
Misko Hevery 2015-01-08 16:17:56 -08:00
parent ee99a5a02b
commit 3b34ef43b1
8 changed files with 128 additions and 52 deletions

View File

@ -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;

View File

@ -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<parts.length; i++) {
var part = parts[i];
if (i%2 === 0) {
// fixed string
ListWrapper.push(strings, part);
} else {
var tokens = this._lexer.tokenize(part);
var ast = new _ParseAST(input, location, tokens, this._reflector, false).parseChain();
ListWrapper.push(expressions, ast);
}
}
return new ASTWithSource(new Interpolation(strings, expressions), input, location);
}
}
class _ParseAST {

View File

@ -14,6 +14,7 @@ import {
Formatter,
FunctionCall,
ImplicitReceiver,
Interpolation,
KeyedAccess,
LiteralArray,
LiteralMap,
@ -116,6 +117,11 @@ class ProtoOperationsCreator {
return 0;
}
visitInterpolation(ast:Interpolation) {
var args = this._visitAll(ast.expressions);
return this._addRecord(RECORD_TYPE_INVOKE_PURE_FUNCTION, "Interpolate()", _interpolationFn(ast.strings), args, 0);
}
visitLiteralPrimitive(ast:LiteralPrimitive) {
return this._addRecord(RECORD_TYPE_CONST, null, ast.value, [], 0);
}
@ -274,4 +280,35 @@ function _cond(cond, trueVal, falseVal) {return cond ? trueVal :
function _keyedAccess(obj, args) {
return obj[args[0]];
}
}
function s(v) {
return isPresent(v) ? '' + v : '';
}
function _interpolationFn(strings:List) {
var length = strings.length;
var i = -1;
var c0 = length > ++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`);
}
}

View File

@ -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');
});
});
});
}

View File

@ -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);
}

View File

@ -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<parts.length; i++) {
var expressionPart = null;
if (i%2 === 0) {
// fixed string
if (parts[i].length > 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);
}
}
}

View File

@ -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('<div a="{{b}}"></div>'));
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()');
});
});
}
}

View File

@ -19,39 +19,39 @@ export function main() {
it('should find text interpolation in normal elements', () => {
var results = createPipeline().process(el('<div>{{expr1}}<span></span>{{expr2}}</div>'));
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('<template>{{expr1}}<span></span>{{expr2}}</template>'));
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('<div>{{expr1}}{{expr2}}</div>'));
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('<div>{{included}}<span ignore-children>{{excluded}}</span></div>'));
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('<div>a{{expr1}}b{{expr2}}c</div>'));
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("<div>'\"a{{expr1}}</div>"));
expect(MapWrapper.get(results[0].textNodeBindings, 0).source).toEqual("'\\'\"a'+(expr1)");
expect(MapWrapper.get(results[0].textNodeBindings, 0).source).toEqual("'\"a{{expr1}}");
});
});
}