[IMP] qweb: implements an expression evaluator

closes #5
This commit is contained in:
Géry Debongnie
2019-06-04 16:21:31 +02:00
parent 438b21df3f
commit 7b887447da
8 changed files with 494 additions and 131 deletions
+11 -5
View File
@@ -175,9 +175,15 @@ root nodes.
### Expression evaluation ### Expression evaluation
It is useful to explain the various rules that applies on QWeb expressions. These QWeb expressions are strings that will be processed at compile time. Each variable in
expressions are strings that will be converted to a javascript expression at the javascript expression will be replaced by a lookup in the context (so, the
compile time. widget). For example, `a + b.c(d)` will be converted into:
```js
context['a'] + context['b'].c(context['d'])
```
It is useful to explain the various rules that applies on these expressions:
1. it should be a simple expression which returns a value. It cannot be a statement. 1. it should be a simple expression which returns a value. It cannot be a statement.
@@ -191,10 +197,10 @@ compile time.
<div><p t-if="console.log(1)">NOT valid</p></div> <div><p t-if="console.log(1)">NOT valid</p></div>
``` ```
2. it can use anything in the rendering context: 2. it can use anything in the rendering context (typically, the component):
```xml ```xml
<p t-if="user.birthday == today()">Happy bithday!</p> <p t-if="user.birthday === today()">Happy bithday!</p>
``` ```
is valid, and will read the `user` object from the context, and call the is valid, and will read the `user` object from the context, and call the
+9 -79
View File
@@ -1,4 +1,5 @@
import { VNode, h } from "./vdom"; import { VNode, h } from "./vdom";
import { QWebVar, compileExpr } from "./qweb_expressions";
/** /**
* Owl QWeb Engine * Owl QWeb Engine
@@ -56,18 +57,6 @@ export interface Directive {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Const/global stuff/helpers // Const/global stuff/helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
};
const DISABLED_TAGS = [ const DISABLED_TAGS = [
"input", "input",
@@ -624,16 +613,6 @@ export class QWeb {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Compilation Context // Compilation Context
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export interface QWebExprVar {
id: string;
expr: string;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
export class Context { export class Context {
nextID: number = 1; nextID: number = 1;
@@ -713,63 +692,14 @@ export class Context {
return val in this.variables ? this.getValue(this.variables[val]) : val; return val in this.variables ? this.getValue(this.variables[val]) : val;
} }
formatExpression(e: string): string { /**
e = e.trim(); * Prepare an expression for being consumed at render time. Its main job
if (e[0] === "{" && e[e.length - 1] === "}") { * is to
const innerExpr = e * - replace unknown variables by a lookup in the context
.slice(1, -1) * - replace already defined variables by their internal name
.split(",") */
.map(p => { formatExpression(expr: string): string {
let [key, val] = p.trim().split(":"); return compileExpr(expr, this.variables);
if (key === "") {
return "";
}
if (!val) {
val = key;
}
return `${key}: ${this.formatExpression(val)}`;
})
.join(",");
return "{" + innerExpr + "}";
}
// Thanks CHM for this code...
const chars = e.split("");
let instring = "";
let invar = "";
let invarPos = 0;
let r = "";
chars.push(" ");
for (let i = 0, ilen = chars.length; i < ilen; i++) {
let c = chars[i];
if (instring.length) {
if (c === instring && chars[i - 1] !== "\\") {
instring = "";
}
} else if (c === '"' || c === "'") {
instring = c;
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
invar = c;
invarPos = i;
continue;
} else if (c.match(/\W/) && invar.length) {
// TODO: Should check for possible spaces before dot
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
invar =
WORD_REPLACEMENT[invar] ||
(this.variables[invar] && (<any>this.variables[invar]).id) ||
"context['" + invar + "']";
}
r += invar;
invar = "";
} else if (invar.length) {
invar += c;
continue;
}
r += c;
}
const result = r.slice(0, -1);
return result;
} }
/** /**
+2 -1
View File
@@ -1,4 +1,5 @@
import { Context, QWeb, UTILS, QWebExprVar } from "./qweb_core"; import { Context, QWeb, UTILS } from "./qweb_core";
import { QWebExprVar } from "./qweb_expressions";
/** /**
* Owl QWeb Directives * Owl QWeb Directives
+270
View File
@@ -0,0 +1,270 @@
/**
* Owl QWeb Expression Parser
*
* Owl needs in various contexts to be able to understand the structure of a
* string representing a javascript expression. The usual goal is to be able
* to rewrite some variables. For example, if a template has
*
* ```xml
* <t t-if="computeSomething({val: state.val})">...</t>
* ```
*
* this needs to be translated in something like this:
*
* ```js
* if (context["computeSomething"]({val: context["state"].val})) { ... }
* ```
*
* This file contains the implementation of an extremely naive tokenizer/parser
* and evaluator for javascript expressions. The supported grammar is basically
* only expressive enough to understand the shape of objects, of arrays, and
* various operators.
*/
//------------------------------------------------------------------------------
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = {
and: "&&",
or: "||",
gt: ">",
gte: ">=",
lt: "<",
lte: "<="
};
export interface QWebExprVar {
id: string;
expr: string;
}
export interface QWebXMLVar {
xml: NodeList;
}
export type QWebVar = QWebExprVar | QWebXMLVar;
//------------------------------------------------------------------------------
// Tokenizer
//------------------------------------------------------------------------------
type TKind =
| "LEFT_BRACE"
| "RIGHT_BRACE"
| "LEFT_BRACKET"
| "RIGHT_BRACKET"
| "LEFT_PAREN"
| "RIGHT_PAREN"
| "COMMA"
| "VALUE"
| "SYMBOL"
| "OPERATOR"
| "COLON";
interface Token {
type: TKind;
value: string;
size?: number;
}
const STATIC_TOKEN_MAP: { [key: string]: TKind } = {
"{": "LEFT_BRACE",
"}": "RIGHT_BRACE",
"[": "LEFT_BRACKET",
"]": "RIGHT_BRACKET",
":": "COLON",
",": "COMMA",
"(": "LEFT_PAREN",
")": "RIGHT_PAREN"
};
const OPERATORS = [".", "===", "==", "+", "!", "||", "&&", ">", "?", "-", "*"];
type Tokenizer = (expr: string) => Token | false;
let tokenizeString: Tokenizer = function(expr) {
let s = expr[0];
let start = s;
if (s !== "'" && s !== '"') {
return false;
}
let i = 1;
let cur;
while (expr[i] && expr[i] !== start) {
cur = expr[i];
s += cur;
if (cur === "\\") {
i++;
cur = expr[i];
if (!cur) {
throw new Error("Invalid expression");
}
s += cur;
}
i++;
}
if (expr[i] !== start) {
throw new Error("Invalid expression");
}
s += start;
return { type: "VALUE", value: s };
};
let tokenizeNumber: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[0-9]/)) {
let i = 1;
while (expr[i] && expr[i].match(/[0-9]|\./)) {
s += expr[i];
i++;
}
return { type: "VALUE", value: s };
} else {
return false;
}
};
let tokenizeSymbol: Tokenizer = function(expr) {
let s = expr[0];
if (s && s.match(/[a-zA-Z_\$]/)) {
let i = 1;
while (expr[i] && expr[i].match(/\w/)) {
s += expr[i];
i++;
}
if (s in WORD_REPLACEMENT) {
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
}
return { type: "SYMBOL", value: s };
} else {
return false;
}
};
const tokenizeStatic: Tokenizer = function(expr) {
const char = expr[0];
if (char && char in STATIC_TOKEN_MAP) {
return { type: STATIC_TOKEN_MAP[char], value: char };
}
return false;
};
const tokenizeOperator: Tokenizer = function(expr) {
for (let op of OPERATORS) {
if (expr.startsWith(op)) {
return { type: "OPERATOR", value: op };
}
}
return false;
};
const TOKENIZERS = [
tokenizeString,
tokenizeNumber,
tokenizeSymbol,
tokenizeStatic,
tokenizeOperator
];
/**
* Convert a javascript expression (as a string) into a list of tokens. For
* example: `tokenize("1 + b")` will return:
* ```js
* [
* {type: "VALUE", value: "1"},
* {type: "OPERATOR", value: "+"},
* {type: "SYMBOL", value: "b"}
* ]
* ```
*/
export function tokenize(expr: string): Token[] {
const result: Token[] = [];
let token: boolean | Token = true;
while (token) {
expr = expr.trim();
if (expr) {
for (let tokenizer of TOKENIZERS) {
token = tokenizer(expr);
if (token) {
result.push(token);
expr = expr.slice(token.size || token.value.length);
break;
}
}
} else {
token = false;
}
}
if (expr.length) {
throw new Error("Tokenizer error...");
}
return result;
}
//------------------------------------------------------------------------------
// Expression "evaluator"
//------------------------------------------------------------------------------
/**
* This is the main function exported by this file. This is the code that will
* process an expression (given as a string) and returns another expression with
* proper lookups in the context.
*
* Usually, this kind of code would be very simple to do if we had an AST (so,
* if we had a javascript parser), since then, we would only need to find the
* variables and replace them. However, a parser is more complicated, and there
* are no standard builtin parser API.
*
* Since this method is applied to simple javasript expressions, and the work to
* be done is actually quite simple, we actually can get away with not using a
* parser, which helps with the code size.
*
* Here is the heuristic used by this method to determine if a token is a
* variable:
* - by default, all symbols are considered a variable
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
* - or if the previous token is a left brace or a comma, and the next token is
* a colon (in that case, this is an object key: `{a: b}`)
*/
export function compileExpr(
expr: string,
vars: { [key: string]: QWebVar }
): string {
const tokens = tokenize(expr);
let result = "";
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
// we need to find if it is a variable
let isVar = true;
let prevToken = tokens[i - 1];
if (prevToken) {
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
isVar = false;
} else if (
prevToken.type === "LEFT_BRACE" ||
prevToken.type === "COMMA"
) {
let nextToken = tokens[i + 1];
if (nextToken && nextToken.type === "COLON") {
isVar = false;
}
}
}
if (isVar) {
if (token.value in vars && "id" in vars[token.value]) {
token.value = (<QWebExprVar>vars[token.value]).id;
} else {
token.value = `context['${token.value}']`;
}
}
}
result += token.value;
}
return result;
}
+2 -2
View File
@@ -54,7 +54,7 @@ exports[`class and style attributes with t-widget t-att-class is properly added/
let _2_index = c1.length; let _2_index = c1.length;
c1.push(null); c1.push(null);
let def3; let def3;
const _5 = {a: context['state'].a,b: context['state'].b}; const _5 = {a:context['state'].a,b:context['state'].b};
let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false; let w4 = 4 in context.__owl__.cmap ? context.__owl__.children[context.__owl__.cmap[4]] : false;
let props4 = {}; let props4 = {};
if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) { if (w4 && w4.__owl__.renderPromise && !w4.__owl__.vnode) {
@@ -373,7 +373,7 @@ exports[`other directives with t-widget t-on with handler bound to object 1`] =
w4 = new W4(owner, props4); w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id; context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare(); def3 = w4._prepare();
def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {val: 3}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;}); def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.addEventListener('ev', owner['onEv'].bind(owner, {val:3}));}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else { } else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
+26 -26
View File
@@ -4,7 +4,7 @@ exports[`attributes class and t-attf-class with ternary operation 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = 'hello ' + (context['value'] ? 'world' : ''); var _1 = 'hello ' + (context['value']?'world':'');
let c2 = [], p2 = {key:2,attrs:{class: _1}}; let c2 = [], p2 = {key:2,attrs:{class: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
return vn2; return vn2;
@@ -70,7 +70,7 @@ exports[`attributes format expression 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = (context['value'] + 37); var _1 = (context['value']+37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
return vn2; return vn2;
@@ -81,7 +81,7 @@ exports[`attributes format expression, other format 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = (context['value'] + 37); var _1 = (context['value']+37);
let c2 = [], p2 = {key:2,attrs:{foo: _1}}; let c2 = [], p2 = {key:2,attrs:{foo: _1}};
var vn2 = h('div', p2, c2); var vn2 = h('div', p2, c2);
return vn2; return vn2;
@@ -127,7 +127,7 @@ exports[`attributes from object variables set previously 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = {a: 'b'}; var _2 = {a:'b'};
var _3 = _2.a; var _3 = _2.a;
let c4 = [], p4 = {key:4,attrs:{class: _3}}; let c4 = [], p4 = {key:4,attrs:{class: _3}};
var vn4 = h('span', p4, c4); var vn4 = h('span', p4, c4);
@@ -223,7 +223,7 @@ exports[`attributes t-att-class with object 1`] = `
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = 'static'; var _1 = 'static';
var _3 = this.utils.objectToAttrString({a: context['b'],c: context['d'],e: context['f']}); var _3 = this.utils.objectToAttrString({a:context['b'],c:context['d'],e:context['f']});
var _2 = 'static' + (_3 ? ' ' + _3 : ''); var _2 = 'static' + (_3 ? ' ' + _3 : '');
let c4 = [], p4 = {key:4,attrs:{class: _2}}; let c4 = [], p4 = {key:4,attrs:{class: _2}};
var vn4 = h('div', p4, c4); var vn4 = h('div', p4, c4);
@@ -246,7 +246,7 @@ exports[`attributes tuple literal 1`] = `
"function anonymous(context,extra "function anonymous(context,extra
) { ) {
var h = this.utils.h; var h = this.utils.h;
var _1 = ['foo', 'bar']; var _1 = ['foo','bar'];
let c2 = [], p2 = {key:2,attrs:{}}; let c2 = [], p2 = {key:2,attrs:{}};
if (_1 instanceof Array) { if (_1 instanceof Array) {
p2.attrs[_1[0]] = _1[1]; p2.attrs[_1[0]] = _1[1];
@@ -303,7 +303,7 @@ exports[`debugging t-log 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 42; var _2 = 42;
console.log(_2 + 3) console.log(_2+3)
return vn1; return vn1;
}" }"
`; `;
@@ -345,7 +345,7 @@ exports[`foreach iterate on items (on a element node) 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [1, 2]; var _2 = [1,2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')} if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2; var _3 = _4 = _2;
if (!(_2 instanceof Array)) { if (!(_2 instanceof Array)) {
@@ -378,7 +378,7 @@ exports[`foreach iterate on items 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [3, 2, 1]; var _2 = [3,2,1];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')} if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2; var _3 = _4 = _2;
if (!(_2 instanceof Array)) { if (!(_2 instanceof Array)) {
@@ -501,7 +501,7 @@ exports[`foreach warn if no key in some case 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = [1, 2]; var _2 = [1,2];
if (!_2) { throw new Error('QWeb error: Invalid loop expression')} if (!_2) { throw new Error('QWeb error: Invalid loop expression')}
var _3 = _4 = _2; var _3 = _4 = _2;
if (!(_2 instanceof Array)) { if (!(_2 instanceof Array)) {
@@ -910,13 +910,13 @@ exports[`t-if boolean value condition elif 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['color'] == 'black') { if (context['color']=='black') {
c1.push({text: \`black pearl\`}); c1.push({text: \`black pearl\`});
} }
else if (context['color'] == 'yellow') { else if (context['color']=='yellow') {
c1.push({text: \`yellow submarine\`}); c1.push({text: \`yellow submarine\`});
} }
else if (context['color'] == 'red') { else if (context['color']=='red') {
c1.push({text: \`red is dead\`}); c1.push({text: \`red is dead\`});
} }
else { else {
@@ -1019,28 +1019,28 @@ exports[`t-if can use some boolean operators in expressions 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
if (context['cond1'] && context['cond2']) { if (context['cond1']&&context['cond2']) {
c1.push({text: \`and\`}); c1.push({text: \`and\`});
} }
if (context['cond1'] && context['cond3']) { if (context['cond1']&&context['cond3']) {
c1.push({text: \`nope\`}); c1.push({text: \`nope\`});
} }
if (context['cond1'] || context['cond3']) { if (context['cond1']||context['cond3']) {
c1.push({text: \`or\`}); c1.push({text: \`or\`});
} }
if (context['cond3'] || context['cond4']) { if (context['cond3']||context['cond4']) {
c1.push({text: \`nope\`}); c1.push({text: \`nope\`});
} }
if (context['m'] > 3) { if (context['m']>3) {
c1.push({text: \`mgt\`}); c1.push({text: \`mgt\`});
} }
if (context['n'] > 3) { if (context['n']>3) {
c1.push({text: \`ngt\`}); c1.push({text: \`ngt\`});
} }
if (context['m'] < 3) { if (context['m']<3) {
c1.push({text: \`mlt\`}); c1.push({text: \`mlt\`});
} }
if (context['n'] < 3) { if (context['n']<3) {
c1.push({text: \`nlt\`}); c1.push({text: \`nlt\`});
} }
return vn1; return vn1;
@@ -1209,7 +1209,7 @@ exports[`t-on can bind handlers with object arguments 1`] = `
if (!context['add']) { if (!context['add']) {
throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`) throw new Error('Missing handler \\\\'' + 'add' + \`\\\\' when evaluating template 'test'\`)
} }
p1.on['click'] = context['add'].bind(owner, {val: 5}); p1.on['click'] = context['add'].bind(owner, {val:5});
c1.push({text: \`Click\`}); c1.push({text: \`Click\`});
return vn1; return vn1;
}" }"
@@ -1536,7 +1536,7 @@ exports[`t-set evaluate value expression 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = 1 + 2; var _2 = 1+2;
if (_2 || _2 === 0) { if (_2 || _2 === 0) {
c1.push({text: _2}); c1.push({text: _2});
} }
@@ -1550,7 +1550,7 @@ exports[`t-set evaluate value expression, part 2 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['somevariable'] + 2; var _2 = context['somevariable']+2;
if (_2 || _2 === 0) { if (_2 || _2 === 0) {
c1.push({text: _2}); c1.push({text: _2});
} }
@@ -1626,7 +1626,7 @@ exports[`t-set t-set and t-if 1`] = `
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['value']; var _2 = context['value'];
if (_2 === 'ok') { if (_2==='ok') {
c1.push({text: \`grimbergen\`}); c1.push({text: \`grimbergen\`});
} }
return vn1; return vn1;
@@ -1639,7 +1639,7 @@ exports[`t-set t-set evaluates an expression only once 1`] = `
var h = this.utils.h; var h = this.utils.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
var vn1 = h('div', p1, c1); var vn1 = h('div', p1, c1);
var _2 = context['value'] + ' artois'; var _2 = context['value']+' artois';
if (_2 || _2 === 0) { if (_2 || _2 === 0) {
c1.push({text: _2}); c1.push({text: _2});
} }
+4 -18
View File
@@ -84,23 +84,6 @@ describe("error handling", () => {
).toThrow("Missing event name with t-on directive"); ).toThrow("Missing event name with t-on directive");
}); });
test("error when compiled code is invalid", () => {
const consoleWarn = console.warn;
const consoleGroupCollapsed = console.groupCollapsed;
console.warn = jest.fn();
console.groupCollapsed = jest.fn();
qweb.addTemplate(
"templatename",
`<div t-att-hey="}/^function invalid{{>'"></div>`
);
expect(() => qweb.render("templatename")).toThrow(
"Invalid generated code while compiling template 'templatename': Unexpected token }"
);
expect(console.warn).toBeCalledTimes(1);
console.warn = consoleWarn;
console.groupCollapsed = consoleGroupCollapsed;
});
test("error when unknown directive", () => { test("error when unknown directive", () => {
qweb.addTemplate( qweb.addTemplate(
"templatename", "templatename",
@@ -530,7 +513,10 @@ describe("attributes", () => {
}); });
test("class and t-attf-class with ternary operation", () => { test("class and t-attf-class with ternary operation", () => {
qweb.addTemplate("test", `<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`); qweb.addTemplate(
"test",
`<div class="hello" t-attf-class="{{value ? 'world' : ''}}"/>`
);
const result = renderToString(qweb, "test", { value: true }); const result = renderToString(qweb, "test", { value: true });
expect(result).toBe(`<div class="hello world"></div>`); expect(result).toBe(`<div class="hello world"></div>`);
}); });
+170
View File
@@ -0,0 +1,170 @@
import { compileExpr, tokenize } from "../src/qweb_expressions";
describe("tokenizer", () => {
test("simple tokens", () => {
expect(tokenize("1.3")).toEqual([
{ type: "VALUE", value: "1.3" }
]);
expect(tokenize("{}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("{ }}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "RIGHT_BRACE", value: "}" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("a")).toEqual([{ type: "SYMBOL", value: "a" }]);
expect(tokenize("true")).toEqual([{ type: "SYMBOL", value: "true" }]);
expect(tokenize("abcde")).toEqual([{ type: "SYMBOL", value: "abcde" }]);
expect(tokenize("_ab2")).toEqual([{ type: "SYMBOL", value: "_ab2" }]);
expect(tokenize("$ab2")).toEqual([{ type: "SYMBOL", value: "$ab2" }]);
expect(tokenize("ABC")).toEqual([{ type: "SYMBOL", value: "ABC" }]);
expect(tokenize("{a: 2}")).toEqual([
{ type: "LEFT_BRACE", value: "{" },
{ type: "SYMBOL", value: "a" },
{ type: "COLON", value: ":" },
{ type: "VALUE", value: "2" },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize("a,")).toEqual([
{ type: "SYMBOL", value: "a" },
{ type: "COMMA", value: "," }
]);
expect(tokenize("][")).toEqual([
{ type: "RIGHT_BRACKET", value: "]" },
{ type: "LEFT_BRACKET", value: "[" }
]);
});
test("strings", () => {
expect(() => tokenize("'")).toThrow("Invalid expression");
expect(() => tokenize("'\\")).toThrow("Invalid expression");
expect(() => tokenize("'\\'")).toThrow("Invalid expression");
expect(tokenize("'hello ged'")).toEqual([
{ type: "VALUE", value: "'hello ged'" }
]);
expect(tokenize("'hello \\'ged\\''")).toEqual([
{ type: "VALUE", value: "'hello \\'ged\\''" }
]);
expect(() => tokenize('"')).toThrow("Invalid expression");
expect(() => tokenize('"\\"')).toThrow("Invalid expression");
expect(tokenize('"hello ged"')).toEqual([
{ type: "VALUE", value: '"hello ged"' }
]);
expect(tokenize('"hello ged"}')).toEqual([
{ type: "VALUE", value: '"hello ged"' },
{ type: "RIGHT_BRACE", value: "}" }
]);
expect(tokenize('"hello \\"ged\\""')).toEqual([
{ type: "VALUE", value: '"hello \\"ged\\""' }
]);
});
});
describe("expression evaluation", () => {
test("simple static values", () => {
expect(compileExpr("1", {})).toBe("1");
expect(compileExpr("1 ", {})).toBe("1");
expect(compileExpr("'some string#/, {' ", {})).toBe("'some string#/, {'");
expect(compileExpr("{ } ", {})).toBe("{}");
expect(compileExpr("{a: 1} ", {})).toBe("{a:1}");
expect(compileExpr("{a: 1, b: 2 } ", {})).toBe("{a:1,b:2}");
expect(compileExpr("[] ", {})).toBe("[]");
expect(compileExpr("[1] ", {})).toBe("[1]");
expect(compileExpr("['1', '2'] ", {})).toBe("['1','2']");
expect(compileExpr("['1', \"2\"] ", {})).toBe("['1',\"2\"]");
});
test("various types of 'words'", () => {
expect(compileExpr("true", {})).toBe("true");
expect(compileExpr("false", {})).toBe("false");
expect(compileExpr("debugger", {})).toBe("debugger");
});
test("parenthesis", () => {
expect(compileExpr("(1)", {})).toBe("(1)");
expect(compileExpr("a*(1 +3)", {})).toBe("context['a']*(1+3)");
});
test("objects and sub objects", () => {
expect(compileExpr("{a:{b:1}} ", {})).toBe("{a:{b:1}}");
});
test("replacing variables", () => {
expect(compileExpr("a", {})).toBe("context['a']");
expect(compileExpr("a", { a: { id: "_3", expr: "" } })).toBe("_3");
});
test("arrays and objects", () => {
expect(compileExpr("[{b:1}] ", {})).toBe("[{b:1}]");
expect(compileExpr("{a: []} ", {})).toBe("{a:[]}");
expect(compileExpr("[{b:1, c: [1, {d: {e: 3}} ]}] ", {})).toBe(
"[{b:1,c:[1,{d:{e:3}}]}]"
);
});
test("dot operator", () => {
expect(compileExpr("a.b", {})).toBe("context['a'].b");
expect(compileExpr("a.b.c", {})).toBe("context['a'].b.c");
});
test("various unary operators", () => {
expect(compileExpr("!flag", {})).toBe("!context['flag']");
expect(compileExpr("-3", {})).toBe("-3");
expect(compileExpr("-a", {})).toBe("-context['a']");
});
test("various binary operators", () => {
expect(compileExpr("color == 'black'", {})).toBe(
"context['color']=='black'"
);
expect(compileExpr("a || b", {})).toBe("context['a']||context['b']");
expect(compileExpr("color === 'black'", {})).toBe(
"context['color']==='black'"
);
expect(compileExpr("'li_'+item", {})).toBe("'li_'+context['item']");
expect(compileExpr("state.val > 1", {})).toBe("context['state'].val>1");
});
test("boolean operations", () => {
expect(compileExpr("a && b", {})).toBe("context['a']&&context['b']");
});
test("ternary operators", () => {
expect(compileExpr("a ? b: '2'", {})).toBe("context['a']?context['b']:'2'");
expect(compileExpr("a ? b: (c or '2') ", {})).toBe(
"context['a']?context['b']:(context['c']||'2')"
);
expect(compileExpr("a ? {test:c}: [1,u]", {})).toBe(
"context['a']?{test:context['c']}:[1,context['u']]"
);
});
test("word replacement", () => {
expect(compileExpr("a or b", {})).toBe("context['a']||context['b']");
expect(compileExpr("a and b", {})).toBe("context['a']&&context['b']");
});
test("function calls", () => {
expect(compileExpr("a()", {})).toBe("context['a']()");
expect(compileExpr("a(1)", {})).toBe("context['a'](1)");
expect(compileExpr("a(1,2)", {})).toBe("context['a'](1,2)");
expect(compileExpr("a(1,2,{a:[a]})", {})).toBe(
"context['a'](1,2,{a:[context['a']]})"
);
expect(compileExpr("'x'.toUpperCase()", {})).toBe("'x'.toUpperCase()");
expect(compileExpr("'x'.toUpperCase({a: 3})", {})).toBe(
"'x'.toUpperCase({a:3})"
);
expect(
compileExpr("'x'.toUpperCase(a)", { a: { id: "_v5", expr: "" } })
).toBe("'x'.toUpperCase(_v5)");
expect(
compileExpr("'x'.toUpperCase({b: a})", { a: { id: "_v5", expr: "" } })
).toBe("'x'.toUpperCase({b:_v5})");
});
});