diff --git a/addons/web/static/lib/owl/owl.js b/addons/web/static/lib/owl/owl.js index fd003f9f25b..bfdf068b0e1 100644 --- a/addons/web/static/lib/owl/owl.js +++ b/addons/web/static/lib/owl/owl.js @@ -155,6 +155,439 @@ } } + /** + * 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 + * ... + * ``` + * + * 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,eval,void,Math,RegExp,Array,Object,Date".split(","); + const WORD_REPLACEMENT = { + and: "&&", + or: "||", + gt: ">", + gte: ">=", + lt: "<", + lte: "<=", + }; + const STATIC_TOKEN_MAP = { + "{": "LEFT_BRACE", + "}": "RIGHT_BRACE", + "[": "LEFT_BRACKET", + "]": "RIGHT_BRACKET", + ":": "COLON", + ",": "COMMA", + "(": "LEFT_PAREN", + ")": "RIGHT_PAREN", + }; + // note that the space after typeof is relevant. It makes sure that the formatted + // expression has a space after typeof + const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(","); + let tokenizeString = 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 = 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 = 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 = 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 = function (expr) { + for (let op of OPERATORS) { + if (expr.startsWith(op)) { + return { type: "OPERATOR", value: op }; + } + } + return false; + }; + const TOKENIZERS = [ + tokenizeString, + tokenizeNumber, + tokenizeOperator, + tokenizeSymbol, + tokenizeStatic, + ]; + /** + * 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"} + * ] + * ``` + */ + function tokenize(expr) { + const result = []; + let 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: could not tokenize "${expr}"`); + } + 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}`) + * + * Some specific code is also required to support arrow functions. If we detect + * the arrow operator, then we add the current (or some previous tokens) token to + * the list of variables so it does not get replaced by a lookup in the context + */ + function compileExprToArray(expr, scope) { + scope = Object.create(scope); + const tokens = tokenize(expr); + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + let prevToken = tokens[i - 1]; + let nextToken = tokens[i + 1]; + let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value); + if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { + if (prevToken) { + if (prevToken.type === "OPERATOR" && prevToken.value === ".") { + isVar = false; + } + else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { + if (nextToken && nextToken.type === "COLON") { + isVar = false; + } + } + } + } + if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") { + if (token.type === "RIGHT_PAREN") { + let j = i - 1; + while (j > 0 && tokens[j].type !== "LEFT_PAREN") { + if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) { + tokens[j].value = tokens[j].originalValue; + scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value }; + } + j--; + } + } + else { + scope[token.value] = { id: token.value, expr: token.value }; + } + } + if (isVar) { + token.varName = token.value; + if (token.value in scope && "id" in scope[token.value]) { + token.value = scope[token.value].expr; + } + else { + token.originalValue = token.value; + token.value = `scope['${token.value}']`; + } + } + } + return tokens; + } + function compileExpr(expr, scope) { + return compileExprToArray(expr, scope) + .map((t) => t.value) + .join(""); + } + + const INTERP_REGEXP = /\{\{.*?\}\}/g; + //------------------------------------------------------------------------------ + // Compilation Context + //------------------------------------------------------------------------------ + class CompilationContext { + constructor(name) { + this.code = []; + this.variables = {}; + this.escaping = false; + this.parentNode = null; + this.parentTextNode = null; + this.rootNode = null; + this.indentLevel = 0; + this.shouldDefineParent = false; + this.shouldDefineScope = false; + this.protectedScopeNumber = 0; + this.shouldDefineQWeb = false; + this.shouldDefineUtils = false; + this.shouldDefineRefs = false; + this.shouldDefineResult = true; + this.loopNumber = 0; + this.inPreTag = false; + this.allowMultipleRoots = false; + this.hasParentWidget = false; + this.hasKey0 = false; + this.keyStack = []; + this.rootContext = this; + this.templateName = name || "noname"; + this.addLine("let h = this.h;"); + } + generateID() { + return CompilationContext.nextID++; + } + /** + * This method generates a "template key", which is basically a unique key + * which depends on the currently set keys, and on the iteration numbers (if + * we are in a loop). + * + * Such a key is necessary when we need to associate an id to some element + * generated by a template (for example, a component) + */ + generateTemplateKey(prefix = "") { + const id = this.generateID(); + if (this.loopNumber === 0 && !this.hasKey0) { + return `'${prefix}__${id}__'`; + } + let key = `\`${prefix}__${id}__`; + let start = this.hasKey0 ? 0 : 1; + for (let i = start; i < this.loopNumber + 1; i++) { + key += `\${key${i}}__`; + } + this.addLine(`let k${id} = ${key}\`;`); + return `k${id}`; + } + generateCode() { + if (this.shouldDefineResult) { + this.code.unshift(" let result;"); + } + if (this.shouldDefineScope) { + this.code.unshift(" let scope = Object.create(context);"); + } + if (this.shouldDefineRefs) { + this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); + } + if (this.shouldDefineParent) { + if (this.hasParentWidget) { + this.code.unshift(" let parent = extra.parent;"); + } + else { + this.code.unshift(" let parent = context;"); + } + } + if (this.shouldDefineQWeb) { + this.code.unshift(" let QWeb = this.constructor;"); + } + if (this.shouldDefineUtils) { + this.code.unshift(" let utils = this.constructor.utils;"); + } + return this.code; + } + withParent(node) { + if (!this.allowMultipleRoots && + this === this.rootContext && + (this.parentNode || this.parentTextNode)) { + throw new Error("A template should not have more than one root node"); + } + if (!this.rootContext.rootNode) { + this.rootContext.rootNode = node; + } + if (!this.parentNode && this.rootContext.shouldDefineResult) { + this.addLine(`result = vn${node};`); + } + return this.subContext("parentNode", node); + } + subContext(key, value) { + const newContext = Object.create(this); + newContext[key] = value; + return newContext; + } + indent() { + this.rootContext.indentLevel++; + } + dedent() { + this.rootContext.indentLevel--; + } + addLine(line) { + const prefix = new Array(this.indentLevel + 2).join(" "); + this.code.push(prefix + line); + return this.code.length - 1; + } + addIf(condition) { + this.addLine(`if (${condition}) {`); + this.indent(); + } + addElse() { + this.dedent(); + this.addLine("} else {"); + this.indent(); + } + closeIf() { + this.dedent(); + this.addLine("}"); + } + getValue(val) { + return val in this.variables ? this.getValue(this.variables[val]) : val; + } + /** + * Prepare an expression for being consumed at render time. Its main job + * is to + * - replace unknown variables by a lookup in the context + * - replace already defined variables by their internal name + */ + formatExpression(expr) { + this.rootContext.shouldDefineScope = true; + return compileExpr(expr, this.variables); + } + captureExpression(expr) { + this.rootContext.shouldDefineScope = true; + const argId = this.generateID(); + const tokens = compileExprToArray(expr, this.variables); + const done = new Set(); + return tokens + .map((tok) => { + if (tok.varName) { + if (!done.has(tok.varName)) { + done.add(tok.varName); + this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`); + } + tok.value = `${tok.varName}_${argId}`; + } + return tok.value; + }) + .join(""); + } + /** + * Perform string interpolation on the given string. Note that if the whole + * string is an expression, it simply returns it (formatted and enclosed in + * parentheses). + * For instance: + * 'Hello {{x}}!' -> `Hello ${x}` + * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') + */ + interpolate(s) { + let matches = s.match(INTERP_REGEXP); + if (matches && matches[0].length === s.length) { + return `(${this.formatExpression(s.slice(2, -2))})`; + } + let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}"); + return "`" + r + "`"; + } + startProtectScope(codeBlock) { + const protectID = this.generateID(); + this.rootContext.protectedScopeNumber++; + this.rootContext.shouldDefineScope = true; + const scopeExpr = `Object.create(scope);`; + this.addLine(`let _origScope${protectID} = scope;`); + this.addLine(`scope = ${scopeExpr}`); + if (!codeBlock) { + this.addLine(`scope.__access_mode__ = 'ro';`); + } + return protectID; + } + stopProtectScope(protectID) { + this.rootContext.protectedScopeNumber--; + this.addLine(`scope = _origScope${protectID};`); + } + } + CompilationContext.nextID = 1; + //------------------------------------------------------------------------------ // module/props.ts //------------------------------------------------------------------------------ @@ -807,439 +1240,6 @@ const patch = init([eventListenersModule, attrsModule, propsModule, classModule]); - /** - * 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 - * ... - * ``` - * - * 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,eval,void,Math,RegExp,Array,Object,Date".split(","); - const WORD_REPLACEMENT = { - and: "&&", - or: "||", - gt: ">", - gte: ">=", - lt: "<", - lte: "<=", - }; - const STATIC_TOKEN_MAP = { - "{": "LEFT_BRACE", - "}": "RIGHT_BRACE", - "[": "LEFT_BRACKET", - "]": "RIGHT_BRACKET", - ":": "COLON", - ",": "COMMA", - "(": "LEFT_PAREN", - ")": "RIGHT_PAREN", - }; - // note that the space after typeof is relevant. It makes sure that the formatted - // expression has a space after typeof - const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(","); - let tokenizeString = 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 = 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 = 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 = 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 = function (expr) { - for (let op of OPERATORS) { - if (expr.startsWith(op)) { - return { type: "OPERATOR", value: op }; - } - } - return false; - }; - const TOKENIZERS = [ - tokenizeString, - tokenizeNumber, - tokenizeOperator, - tokenizeSymbol, - tokenizeStatic, - ]; - /** - * 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"} - * ] - * ``` - */ - function tokenize(expr) { - const result = []; - let 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: could not tokenize "${expr}"`); - } - 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}`) - * - * Some specific code is also required to support arrow functions. If we detect - * the arrow operator, then we add the current (or some previous tokens) token to - * the list of variables so it does not get replaced by a lookup in the context - */ - function compileExprToArray(expr, scope) { - scope = Object.create(scope); - const tokens = tokenize(expr); - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; - let prevToken = tokens[i - 1]; - let nextToken = tokens[i + 1]; - let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value); - if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) { - if (prevToken) { - if (prevToken.type === "OPERATOR" && prevToken.value === ".") { - isVar = false; - } - else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") { - if (nextToken && nextToken.type === "COLON") { - isVar = false; - } - } - } - } - if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") { - if (token.type === "RIGHT_PAREN") { - let j = i - 1; - while (j > 0 && tokens[j].type !== "LEFT_PAREN") { - if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) { - tokens[j].value = tokens[j].originalValue; - scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value }; - } - j--; - } - } - else { - scope[token.value] = { id: token.value, expr: token.value }; - } - } - if (isVar) { - token.varName = token.value; - if (token.value in scope && "id" in scope[token.value]) { - token.value = scope[token.value].expr; - } - else { - token.originalValue = token.value; - token.value = `scope['${token.value}']`; - } - } - } - return tokens; - } - function compileExpr(expr, scope) { - return compileExprToArray(expr, scope) - .map((t) => t.value) - .join(""); - } - - const INTERP_REGEXP = /\{\{.*?\}\}/g; - //------------------------------------------------------------------------------ - // Compilation Context - //------------------------------------------------------------------------------ - class CompilationContext { - constructor(name) { - this.code = []; - this.variables = {}; - this.escaping = false; - this.parentNode = null; - this.parentTextNode = null; - this.rootNode = null; - this.indentLevel = 0; - this.shouldDefineParent = false; - this.shouldDefineScope = false; - this.protectedScopeNumber = 0; - this.shouldDefineQWeb = false; - this.shouldDefineUtils = false; - this.shouldDefineRefs = false; - this.shouldDefineResult = true; - this.loopNumber = 0; - this.inPreTag = false; - this.allowMultipleRoots = false; - this.hasParentWidget = false; - this.hasKey0 = false; - this.keyStack = []; - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("let h = this.h;"); - } - generateID() { - return CompilationContext.nextID++; - } - /** - * This method generates a "template key", which is basically a unique key - * which depends on the currently set keys, and on the iteration numbers (if - * we are in a loop). - * - * Such a key is necessary when we need to associate an id to some element - * generated by a template (for example, a component) - */ - generateTemplateKey(prefix = "") { - const id = this.generateID(); - if (this.loopNumber === 0 && !this.hasKey0) { - return `'${prefix}__${id}__'`; - } - let key = `\`${prefix}__${id}__`; - let start = this.hasKey0 ? 0 : 1; - for (let i = start; i < this.loopNumber + 1; i++) { - key += `\${key${i}}__`; - } - this.addLine(`let k${id} = ${key}\`;`); - return `k${id}`; - } - generateCode() { - if (this.shouldDefineResult) { - this.code.unshift(" let result;"); - } - if (this.shouldDefineScope) { - this.code.unshift(" let scope = Object.create(context);"); - } - if (this.shouldDefineRefs) { - this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); - } - if (this.shouldDefineParent) { - if (this.hasParentWidget) { - this.code.unshift(" let parent = extra.parent;"); - } - else { - this.code.unshift(" let parent = context;"); - } - } - if (this.shouldDefineQWeb) { - this.code.unshift(" let QWeb = this.constructor;"); - } - if (this.shouldDefineUtils) { - this.code.unshift(" let utils = this.constructor.utils;"); - } - return this.code; - } - withParent(node) { - if (!this.allowMultipleRoots && - this === this.rootContext && - (this.parentNode || this.parentTextNode)) { - throw new Error("A template should not have more than one root node"); - } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; - } - if (!this.parentNode && this.rootContext.shouldDefineResult) { - this.addLine(`result = vn${node};`); - } - return this.subContext("parentNode", node); - } - subContext(key, value) { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - indent() { - this.rootContext.indentLevel++; - } - dedent() { - this.rootContext.indentLevel--; - } - addLine(line) { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - return this.code.length - 1; - } - addIf(condition) { - this.addLine(`if (${condition}) {`); - this.indent(); - } - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); - } - closeIf() { - this.dedent(); - this.addLine("}"); - } - getValue(val) { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } - /** - * Prepare an expression for being consumed at render time. Its main job - * is to - * - replace unknown variables by a lookup in the context - * - replace already defined variables by their internal name - */ - formatExpression(expr) { - this.rootContext.shouldDefineScope = true; - return compileExpr(expr, this.variables); - } - captureExpression(expr) { - this.rootContext.shouldDefineScope = true; - const argId = this.generateID(); - const tokens = compileExprToArray(expr, this.variables); - const done = new Set(); - return tokens - .map((tok) => { - if (tok.varName) { - if (!done.has(tok.varName)) { - done.add(tok.varName); - this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`); - } - tok.value = `${tok.varName}_${argId}`; - } - return tok.value; - }) - .join(""); - } - /** - * Perform string interpolation on the given string. Note that if the whole - * string is an expression, it simply returns it (formatted and enclosed in - * parentheses). - * For instance: - * 'Hello {{x}}!' -> `Hello ${x}` - * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') - */ - interpolate(s) { - let matches = s.match(INTERP_REGEXP); - if (matches && matches[0].length === s.length) { - return `(${this.formatExpression(s.slice(2, -2))})`; - } - let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}"); - return "`" + r + "`"; - } - startProtectScope(codeBlock) { - const protectID = this.generateID(); - this.rootContext.protectedScopeNumber++; - this.rootContext.shouldDefineScope = true; - const scopeExpr = `Object.create(scope);`; - this.addLine(`let _origScope${protectID} = scope;`); - this.addLine(`scope = ${scopeExpr}`); - if (!codeBlock) { - this.addLine(`scope.__access_mode__ = 'ro';`); - } - return protectID; - } - stopProtectScope(protectID) { - this.rootContext.protectedScopeNumber--; - this.addLine(`scope = _origScope${protectID};`); - } - } - CompilationContext.nextID = 1; - const browser = { setTimeout: window.setTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window), @@ -1555,7 +1555,7 @@ const template = { elem, fn: function (context, extra) { - const compiledFunction = this._compile(name, elem); + const compiledFunction = this._compile(name); template.fn = compiledFunction; return compiledFunction.call(this, context, extra); }, @@ -1649,26 +1649,27 @@ } }); } - _compile(name, elem, parentContext, defineKey) { + _compile(name, options = {}) { + const elem = options.elem || this.templates[name].elem; const isDebug = elem.attributes.hasOwnProperty("t-debug"); const ctx = new CompilationContext(name); if (elem.tagName !== "t") { ctx.shouldDefineResult = false; } - if (parentContext) { - ctx.variables = Object.create(parentContext.variables); - ctx.parentNode = parentContext.parentNode || ctx.generateID(); + if (options.hasParent) { + ctx.variables = Object.create(null); + ctx.parentNode = ctx.generateID(); ctx.allowMultipleRoots = true; ctx.hasParentWidget = true; ctx.shouldDefineResult = false; ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`); - if (defineKey) { + if (options.defineKey) { ctx.addLine(`let key0 = extra.key || "";`); ctx.hasKey0 = true; } } this._compileNode(elem, ctx); - if (!parentContext) { + if (!options.hasParent) { if (ctx.shouldDefineResult) { ctx.addLine(`return result;`); } @@ -2065,6 +2066,7 @@ QWeb.nextId = 1; // dev mode enables better error messages or more costly validations QWeb.dev = false; + QWeb.enableTransitions = true; // slots contains sub templates defined with t-set inside t-component nodes, and // are meant to be used by the t-slot directive. QWeb.slots = {}; @@ -2082,6 +2084,9 @@ } function htmlToVNode(node) { if (!(node instanceof Element)) { + if (node instanceof Comment) { + return h("!", node.textContent); + } return { text: node.textContent }; } const attrs = {}; @@ -2302,18 +2307,34 @@ ctx.rootContext.shouldDefineScope = true; ctx.rootContext.shouldDefineUtils = true; const subTemplate = node.getAttribute("t-call"); + const isDynamic = INTERP_REGEXP.test(subTemplate); const nodeTemplate = qweb.templates[subTemplate]; - if (!nodeTemplate) { + if (!isDynamic && !nodeTemplate) { throw new Error(`Cannot find template "${subTemplate}" (t-call)`); } // Step 2: compile target template in sub templates // ------------------------------------------------ - let subId = qweb.subTemplates[subTemplate]; - if (!subId) { - subId = QWeb.nextId++; - qweb.subTemplates[subTemplate] = subId; - const subTemplateFn = qweb._compile(subTemplate, nodeTemplate.elem, ctx, true); - QWeb.subTemplates[subId] = subTemplateFn; + let subIdstr; + if (isDynamic) { + const _id = ctx.generateID(); + ctx.addLine(`let tname${_id} = ${ctx.interpolate(subTemplate)};`); + ctx.addLine(`let tid${_id} = this.subTemplates[tname${_id}];`); + ctx.addIf(`!tid${_id}`); + ctx.addLine(`tid${_id} = this.constructor.nextId++;`); + ctx.addLine(`this.subTemplates[tname${_id}] = tid${_id};`); + ctx.addLine(`this.constructor.subTemplates[tid${_id}] = this._compile(tname${_id}, {hasParent: true, defineKey: true});`); + ctx.closeIf(); + subIdstr = `tid${_id}`; + } + else { + let subId = qweb.subTemplates[subTemplate]; + if (!subId) { + subId = QWeb.nextId++; + qweb.subTemplates[subTemplate] = subId; + const subTemplateFn = qweb._compile(subTemplate, { hasParent: true, defineKey: true }); + QWeb.subTemplates[subId] = subTemplateFn; + } + subIdstr = `'${subId}'`; } // Step 3: compile t-call body if necessary // ------------------------------------------------ @@ -2327,16 +2348,13 @@ for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) { nodeCopy.removeAttribute(attr); } - const parentNode = ctx.parentNode; - ctx.parentNode = "__0"; // this local scope is intended to trap c__0 ctx.addLine(`{`); ctx.indent(); ctx.addLine("let c__0 = [];"); - qweb._compileNode(nodeCopy, ctx); + qweb._compileNode(nodeCopy, ctx.subContext("parentNode", "__0")); ctx.rootContext.shouldDefineUtils = true; ctx.addLine("scope[utils.zero] = c__0;"); - ctx.parentNode = parentNode; ctx.dedent(); ctx.addLine(`}`); } @@ -2347,13 +2365,13 @@ const parentNode = ctx.parentNode ? `c${ctx.parentNode}` : "result"; const extra = `Object.assign({}, extra, {parentNode: ${parentNode}, parent: ${parentComponent}, key: ${key}})`; if (ctx.parentNode) { - ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`); + ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`); } else { // this is a t-call with no parentnode, we need to extract the result ctx.rootContext.shouldDefineResult = true; ctx.addLine(`result = []`); - ctx.addLine(`this.constructor.subTemplates['${subId}'].call(this, scope, ${extra});`); + ctx.addLine(`this.constructor.subTemplates[${subIdstr}].call(this, scope, ${extra});`); ctx.addLine(`result = result[0]`); } // Step 5: restore previous scope @@ -2620,6 +2638,9 @@ name: "transition", priority: 96, atNodeCreation({ ctx, value, addNodeHook }) { + if (!QWeb.enableTransitions) { + return; + } ctx.rootContext.shouldDefineUtils = true; let name = value; const hooks = { @@ -2785,6 +2806,14 @@ } }, }); + Object.defineProperty(config, "enableTransitions", { + get() { + return QWeb.enableTransitions; + }, + set(value) { + QWeb.enableTransitions = value; + }, + }); /** * We define here OwlEvent, a subclass of CustomEvent, with an additional @@ -3020,7 +3049,9 @@ events.push([name, value]); } else if (name === "t-transition") { - transition = value; + if (QWeb.enableTransitions) { + transition = value; + } } else if (!name.startsWith("t-")) { if (name !== "class" && name !== "style") { @@ -3165,7 +3196,6 @@ ctx.addLine(`parent.__owl__.cmap[${templateKey}] = w${componentID}.__owl__.id;`); if (hasSlots) { const clone = node.cloneNode(true); - const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]")); // The next code is a fallback for compatibility reason. It accepts t-set // elements that are direct children with a non empty body as nodes defining // the content of a slot. @@ -3174,26 +3204,42 @@ // code using slots. This will be removed in v2.0 someday. Meanwhile, // please use t-set-slot everywhere you need to set the content of a // slot. - for (let el of clone.children) { - if (el.getAttribute("t-set") && el.hasChildNodes()) { - slotNodes.push(el); + for (let node of clone.children) { + if (node.hasAttribute("t-set") && node.hasChildNodes()) { + node.setAttribute("t-set-slot", node.getAttribute("t-set")); + node.removeAttribute("t-set"); } } + const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]")); + const slotNames = new Set(); const slotId = QWeb.nextSlotId++; ctx.addLine(`w${componentID}.__owl__.slotId = ${slotId};`); if (slotNodes.length) { for (let i = 0, length = slotNodes.length; i < length; i++) { const slotNode = slotNodes[i]; - slotNode.parentElement.removeChild(slotNode); - let key = slotNode.getAttribute("t-set-slot"); - slotNode.removeAttribute("t-set-slot"); - // here again, this code should be removed when we stop supporting - // using t-set to define the content of named slots. - if (!key) { - key = slotNode.getAttribute("t-set"); - slotNode.removeAttribute("t-set"); + // check if this is defined in a sub component (in which case it should + // be ignored) + let el = slotNode.parentElement; + let isInSubComponent = false; + while (el !== clone) { + if (el.hasAttribute("t-component") || + el.tagName[0] === el.tagName[0].toUpperCase()) { + isInSubComponent = true; + break; + } + el = el.parentElement; } - const slotFn = qweb._compile(`slot_${key}_template`, slotNode, ctx); + if (isInSubComponent) { + continue; + } + let key = slotNode.getAttribute("t-set-slot"); + if (slotNames.has(key)) { + continue; + } + slotNames.add(key); + slotNode.removeAttribute("t-set-slot"); + slotNode.parentElement.removeChild(slotNode); + const slotFn = qweb._compile(`slot_${key}_template`, { elem: slotNode, hasParent: true }); QWeb.slots[`${slotId}_${key}`] = slotFn; } } @@ -3202,7 +3248,7 @@ for (let child of Object.values(clone.childNodes)) { t.appendChild(child); } - const slotFn = qweb._compile(`slot_default_template`, t, ctx); + const slotFn = qweb._compile(`slot_default_template`, { elem: t, hasParent: true }); QWeb.slots[`${slotId}_default`] = slotFn; } } @@ -5268,9 +5314,11 @@ exports.useState = useState$1; exports.utils = utils; - exports.__info__.version = '1.0.10'; - exports.__info__.date = '2020-10-02T07:33:29.701Z'; - exports.__info__.hash = 'e73fb46'; - exports.__info__.url = 'https://github.com/odoo/owl'; + + __info__.version = '1.0.13'; + __info__.date = '2020-10-26T07:38:42.693Z'; + __info__.hash = 'd615ffd'; + __info__.url = 'https://github.com/odoo/owl'; + }(this.owl = this.owl || {}));