perf(CD): Special cased interpolation in AST, Parser, and CD
This commit is contained in:
parent
ee99a5a02b
commit
3b34ef43b1
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}}");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue