mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[REF] initial prototype of owl 2
This commit is contained in:
committed by
Aaron Bohy
parent
c06049076a
commit
e746574a1d
@@ -1,395 +0,0 @@
|
||||
import { CompilationContext, INTERP_REGEXP } from "./compilation_context";
|
||||
import { QWeb } from "./qweb";
|
||||
import { htmlToVDOM } from "../vdom/html_to_vdom";
|
||||
import { QWebVar } from "./expression_parser";
|
||||
|
||||
/**
|
||||
* Owl QWeb Directives
|
||||
*
|
||||
* This file contains the implementation of most standard QWeb directives:
|
||||
* - t-esc
|
||||
* - t-raw
|
||||
* - t-set/t-value
|
||||
* - t-if/t-elif/t-else
|
||||
* - t-call
|
||||
* - t-foreach/t-as
|
||||
* - t-debug
|
||||
* - t-log
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-esc and t-raw
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.htmlToVDOM = htmlToVDOM;
|
||||
|
||||
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: CompilationContext) {
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
if (value === "0") {
|
||||
if (ctx.parentNode) {
|
||||
// the 'zero' magical symbol is where we can find the result of the rendering
|
||||
// of the body of the t-call.
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
const zeroArgs = ctx.escaping
|
||||
? `{text: utils.vDomToString(scope[utils.zero])}`
|
||||
: `...scope[utils.zero]`;
|
||||
ctx.addLine(`c${ctx.parentNode}.push(${zeroArgs});`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let exprID: string;
|
||||
if (typeof value === "string") {
|
||||
exprID = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${exprID} = ${ctx.formatExpression(value)};`);
|
||||
} else {
|
||||
exprID = `scope.${value.id}`;
|
||||
}
|
||||
ctx.addIf(`${exprID} != null`);
|
||||
|
||||
if (ctx.escaping) {
|
||||
let protectID;
|
||||
if (value.hasBody) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
protectID = ctx.startProtectScope();
|
||||
ctx.addLine(
|
||||
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
|
||||
);
|
||||
}
|
||||
if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
|
||||
} else if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
|
||||
} else {
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
|
||||
if (ctx.rootContext.shouldDefineResult) {
|
||||
ctx.addLine(`result = vn${nodeID}`);
|
||||
}
|
||||
}
|
||||
if (value.hasBody) {
|
||||
ctx.stopProtectScope(protectID);
|
||||
}
|
||||
} else {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
if (value.hasBody) {
|
||||
ctx.addLine(
|
||||
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
|
||||
);
|
||||
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
|
||||
} else {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
|
||||
}
|
||||
}
|
||||
if (node.childNodes.length) {
|
||||
ctx.addElse();
|
||||
qweb._compileChildren(node, ctx);
|
||||
}
|
||||
|
||||
ctx.closeIf();
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "esc",
|
||||
priority: 70,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let value = ctx.getValue(node.getAttribute("t-esc")!);
|
||||
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "raw",
|
||||
priority: 80,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
let value = ctx.getValue(node.getAttribute("t-raw")!);
|
||||
compileValueNode(value, node, qweb, ctx);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-set
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "set",
|
||||
extraNames: ["value"],
|
||||
priority: 60,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
const variable = node.getAttribute("t-set")!;
|
||||
let value = node.getAttribute("t-value")!;
|
||||
ctx.variables[variable] = ctx.variables[variable] || ({} as QWebVar);
|
||||
let qwebvar = ctx.variables[variable];
|
||||
const hasBody = node.hasChildNodes();
|
||||
|
||||
qwebvar.id = variable;
|
||||
qwebvar.expr = `scope.${variable}`;
|
||||
if (value) {
|
||||
const formattedValue = ctx.formatExpression(value);
|
||||
let scopeExpr = `scope`;
|
||||
if (ctx.protectedScopeNumber) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
scopeExpr = `utils.getScope(scope, '${variable}')`;
|
||||
}
|
||||
ctx.addLine(`${scopeExpr}.${variable} = ${formattedValue};`);
|
||||
qwebvar.value = formattedValue;
|
||||
}
|
||||
|
||||
if (hasBody) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
if (value) {
|
||||
ctx.addIf(`!(${qwebvar.expr})`);
|
||||
}
|
||||
const tempParentNodeID = ctx.generateID();
|
||||
const _parentNode = ctx.parentNode;
|
||||
ctx.parentNode = tempParentNodeID;
|
||||
|
||||
ctx.addLine(`let c${tempParentNodeID} = new utils.VDomArray();`);
|
||||
const nodeCopy = node.cloneNode(true) as Element;
|
||||
for (let attr of ["t-set", "t-value", "t-if", "t-else", "t-elif"]) {
|
||||
nodeCopy.removeAttribute(attr);
|
||||
}
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
|
||||
ctx.addLine(`${qwebvar.expr} = c${tempParentNodeID}`);
|
||||
qwebvar.value = `c${tempParentNodeID}`;
|
||||
qwebvar.hasBody = true;
|
||||
|
||||
ctx.parentNode = _parentNode;
|
||||
if (value) {
|
||||
ctx.closeIf();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-if, t-elif, t-else
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "if",
|
||||
priority: 20,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-if")!);
|
||||
ctx.addIf(typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id!}`);
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
},
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "elif",
|
||||
priority: 30,
|
||||
atNodeEncounter({ node, ctx }): boolean {
|
||||
let cond = ctx.getValue(node.getAttribute("t-elif")!);
|
||||
ctx.addLine(
|
||||
`else if (${typeof cond === "string" ? ctx.formatExpression(cond) : `scope.${cond.id}`}) {`
|
||||
);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
},
|
||||
});
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "else",
|
||||
priority: 40,
|
||||
atNodeEncounter({ ctx }): boolean {
|
||||
ctx.addLine(`else {`);
|
||||
ctx.indent();
|
||||
return false;
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.closeIf();
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-call
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "call",
|
||||
priority: 50,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
// Step 1: sanity checks
|
||||
// ------------------------------------------------
|
||||
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 (!isDynamic && !nodeTemplate) {
|
||||
throw new Error(`Cannot find template "${subTemplate}" (t-call)`);
|
||||
}
|
||||
|
||||
// Step 2: compile target template in sub templates
|
||||
// ------------------------------------------------
|
||||
let subIdstr: string;
|
||||
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
|
||||
// ------------------------------------------------
|
||||
let hasBody = node.hasChildNodes();
|
||||
const protectID = ctx.startProtectScope();
|
||||
if (hasBody) {
|
||||
// we add a sub scope to protect the ambient scope
|
||||
ctx.addLine(`{`);
|
||||
ctx.indent();
|
||||
const nodeCopy = node.cloneNode(true) as Element;
|
||||
for (let attr of ["t-if", "t-else", "t-elif", "t-call"]) {
|
||||
nodeCopy.removeAttribute(attr);
|
||||
}
|
||||
// this local scope is intended to trap c__0
|
||||
ctx.addLine(`{`);
|
||||
ctx.indent();
|
||||
ctx.addLine("let c__0 = [];");
|
||||
qweb._compileNode(nodeCopy, ctx.subContext("parentNode", "__0"));
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
ctx.addLine("scope[utils.zero] = c__0;");
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
|
||||
// Step 4: add the appropriate function call to current component
|
||||
// ------------------------------------------------
|
||||
const parentComponent = ctx.rootContext.shouldDefineParent
|
||||
? `parent`
|
||||
: `utils.getComponent(context)`;
|
||||
const key = ctx.generateTemplateKey();
|
||||
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[${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[${subIdstr}].call(this, scope, ${extra});`);
|
||||
ctx.addLine(`result = result[0]`);
|
||||
}
|
||||
|
||||
// Step 5: restore previous scope
|
||||
// ------------------------------------------------
|
||||
if (hasBody) {
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
}
|
||||
ctx.stopProtectScope(protectID);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-foreach
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "foreach",
|
||||
extraNames: ["as"],
|
||||
priority: 10,
|
||||
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||
ctx.rootContext.shouldDefineScope = true;
|
||||
ctx = ctx.subContext("loopNumber", ctx.loopNumber + 1);
|
||||
const elems = node.getAttribute("t-foreach")!;
|
||||
const name = node.getAttribute("t-as")!;
|
||||
let arrayID = ctx.generateID();
|
||||
ctx.addLine(`let _${arrayID} = ${ctx.formatExpression(elems)};`);
|
||||
ctx.addLine(`if (!_${arrayID}) { throw new Error('QWeb error: Invalid loop expression')}`);
|
||||
let keysID = ctx.generateID();
|
||||
let valuesID = ctx.generateID();
|
||||
ctx.addLine(`let _${keysID} = _${arrayID};`);
|
||||
ctx.addLine(`let _${valuesID} = _${arrayID};`);
|
||||
ctx.addIf(`!(_${arrayID} instanceof Array)`);
|
||||
ctx.addLine(`_${keysID} = Object.keys(_${arrayID});`);
|
||||
ctx.addLine(`_${valuesID} = Object.values(_${arrayID});`);
|
||||
ctx.closeIf();
|
||||
ctx.addLine(`let _length${keysID} = _${keysID}.length;`);
|
||||
let varsID = ctx.startProtectScope(true);
|
||||
const loopVar = `i${ctx.loopNumber}`;
|
||||
ctx.addLine(`for (let ${loopVar} = 0; ${loopVar} < _length${keysID}; ${loopVar}++) {`);
|
||||
ctx.indent();
|
||||
|
||||
ctx.addLine(`scope.${name}_first = ${loopVar} === 0`);
|
||||
ctx.addLine(`scope.${name}_last = ${loopVar} === _length${keysID} - 1`);
|
||||
ctx.addLine(`scope.${name}_index = ${loopVar}`);
|
||||
ctx.addLine(`scope.${name} = _${keysID}[${loopVar}]`);
|
||||
ctx.addLine(`scope.${name}_value = _${valuesID}[${loopVar}]`);
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
let shouldWarn =
|
||||
!nodeCopy.hasAttribute("t-key") &&
|
||||
node.children.length === 1 &&
|
||||
node.children[0].tagName !== "t" &&
|
||||
!node.children[0].hasAttribute("t-key");
|
||||
if (shouldWarn) {
|
||||
console.warn(
|
||||
`Directive t-foreach should always be used with a t-key! (in template: '${ctx.templateName}')`
|
||||
);
|
||||
}
|
||||
if (nodeCopy.hasAttribute("t-key")) {
|
||||
const expr = ctx.formatExpression(nodeCopy.getAttribute("t-key")!);
|
||||
ctx.addLine(`let key${ctx.loopNumber} = ${expr};`);
|
||||
nodeCopy.removeAttribute("t-key");
|
||||
} else {
|
||||
ctx.addLine(`let key${ctx.loopNumber} = i${ctx.loopNumber};`);
|
||||
}
|
||||
|
||||
nodeCopy.removeAttribute("t-foreach");
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
ctx.stopProtectScope(varsID);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-debug
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "debug",
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx }) {
|
||||
ctx.addLine("debugger;");
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-log
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "log",
|
||||
priority: 1,
|
||||
atNodeEncounter({ ctx, value }) {
|
||||
const expr = ctx.formatExpression(value);
|
||||
ctx.addLine(`console.log(${expr})`);
|
||||
},
|
||||
});
|
||||
@@ -1,229 +0,0 @@
|
||||
import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||
//------------------------------------------------------------------------------
|
||||
// Compilation Context
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class CompilationContext {
|
||||
static nextID: number = 1;
|
||||
code: string[] = [];
|
||||
variables: { [key: string]: QWebVar } = {};
|
||||
escaping: boolean = false;
|
||||
parentNode: number | null | string = null;
|
||||
parentTextNode: number | null = null;
|
||||
rootNode: number | null = null;
|
||||
indentLevel: number = 0;
|
||||
rootContext: CompilationContext;
|
||||
shouldDefineParent: boolean = false;
|
||||
shouldDefineScope: boolean = false;
|
||||
protectedScopeNumber: number = 0;
|
||||
shouldDefineQWeb: boolean = false;
|
||||
shouldDefineUtils: boolean = false;
|
||||
shouldDefineRefs: boolean = false;
|
||||
shouldDefineResult: boolean = true;
|
||||
loopNumber: number = 0;
|
||||
inPreTag: boolean = false;
|
||||
templateName: string;
|
||||
allowMultipleRoots: boolean = false;
|
||||
hasParentWidget: boolean = false;
|
||||
hasKey0: boolean = false;
|
||||
keyStack: boolean[] = [];
|
||||
|
||||
constructor(name?: string) {
|
||||
this.rootContext = this;
|
||||
this.templateName = name || "noname";
|
||||
this.addLine("let h = this.h;");
|
||||
}
|
||||
|
||||
generateID(): number {
|
||||
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: string = ""): string {
|
||||
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(): string[] {
|
||||
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: number): CompilationContext {
|
||||
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: keyof CompilationContext, value: any): CompilationContext {
|
||||
const newContext = Object.create(this);
|
||||
newContext[key] = value;
|
||||
return newContext;
|
||||
}
|
||||
|
||||
indent() {
|
||||
this.rootContext.indentLevel++;
|
||||
}
|
||||
|
||||
dedent() {
|
||||
this.rootContext.indentLevel--;
|
||||
}
|
||||
|
||||
addLine(line: string): number {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
this.code.push(prefix + line);
|
||||
return this.code.length - 1;
|
||||
}
|
||||
|
||||
addIf(condition: string) {
|
||||
this.addLine(`if (${condition}) {`);
|
||||
this.indent();
|
||||
}
|
||||
|
||||
addElse() {
|
||||
this.dedent();
|
||||
this.addLine("} else {");
|
||||
this.indent();
|
||||
}
|
||||
|
||||
closeIf() {
|
||||
this.dedent();
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
getValue(val: any): QWebVar | string {
|
||||
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: string): string {
|
||||
this.rootContext.shouldDefineScope = true;
|
||||
return compileExpr(expr, this.variables);
|
||||
}
|
||||
captureExpression(expr: string): string {
|
||||
this.rootContext.shouldDefineScope = true;
|
||||
const argId = this.generateID();
|
||||
const tokens = compileExprToArray(expr, this.variables);
|
||||
const done = new Set();
|
||||
return tokens
|
||||
.map((tok, i) => {
|
||||
// "this" in captured expressions should be the current component
|
||||
if (tok.value === "this") {
|
||||
if (!done.has("this")) {
|
||||
done.add("this");
|
||||
this.addLine(`const this_${argId} = utils.getComponent(context);`);
|
||||
}
|
||||
tok.value = `this_${argId}`;
|
||||
}
|
||||
// Variables that should be looked up in the scope. isLocal is for arrow
|
||||
// function arguments that should stay untouched (eg "ev => ev" should
|
||||
// not become "const ev_1 = scope['ev']; ev_1 => ev_1")
|
||||
if (
|
||||
tok.varName &&
|
||||
!tok.isLocal &&
|
||||
// HACK: for backwards compatibility, we don't capture bare methods
|
||||
// this allows them to be called with the rendering context/scope
|
||||
// as their this value.
|
||||
(!tokens[i + 1] || tokens[i + 1].type !== "LEFT_PAREN")
|
||||
) {
|
||||
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: string): string {
|
||||
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?: boolean): number {
|
||||
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: number) {
|
||||
this.rootContext.protectedScopeNumber--;
|
||||
this.addLine(`scope = _origScope${protectID};`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,982 @@
|
||||
import { BDom } from "../blockdom";
|
||||
import { Dom, DomNode, domToString, DomType } from "./dom_helpers";
|
||||
import { compileExpr, compileExprToArray, interpolate, INTERP_REGEXP } from "./inline_expressions";
|
||||
import {
|
||||
AST,
|
||||
ASTComment,
|
||||
ASTComponent,
|
||||
ASTDebug,
|
||||
ASTDomNode,
|
||||
ASTLog,
|
||||
ASTMulti,
|
||||
ASTSlot,
|
||||
ASTTCall,
|
||||
ASTTCallBlock,
|
||||
ASTTEsc,
|
||||
ASTText,
|
||||
ASTTForEach,
|
||||
ASTTif,
|
||||
ASTTKey,
|
||||
ASTTRaw,
|
||||
ASTTSet,
|
||||
ASTType,
|
||||
parse,
|
||||
} from "./parser";
|
||||
|
||||
export type Template = (context: any, vnode: any, key?: string) => BDom;
|
||||
export type TemplateFunction = (blocks: any, utils: any) => Template;
|
||||
|
||||
type BlockType = "block" | "text" | "multi" | "list" | "html";
|
||||
|
||||
export function compileTemplate(template: string, name?: string): TemplateFunction {
|
||||
const compiler = new QWebCompiler(template, name);
|
||||
return compiler.compile();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// BlockDescription
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class BlockDescription {
|
||||
static nextBlockId = 1;
|
||||
static nextDataId = 1;
|
||||
|
||||
varName: string;
|
||||
blockName: string;
|
||||
isRoot: boolean = false;
|
||||
hasDynamicChildren: boolean = false;
|
||||
children: BlockDescription[] = [];
|
||||
data: string[] = [];
|
||||
dom?: Dom;
|
||||
currentDom?: DomNode;
|
||||
childNumber: number = 0;
|
||||
target: CodeTarget;
|
||||
type: BlockType;
|
||||
parentVar: string = "";
|
||||
id: number;
|
||||
|
||||
constructor(target: CodeTarget, type: BlockType) {
|
||||
this.id = BlockDescription.nextBlockId++;
|
||||
this.varName = "b" + this.id;
|
||||
this.blockName = "block" + this.id;
|
||||
this.target = target;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
insertData(str: string): number {
|
||||
const id = "d" + BlockDescription.nextDataId++;
|
||||
this.target.addLine(`let ${id} = ${str};`);
|
||||
return this.data.push(id) - 1;
|
||||
}
|
||||
|
||||
insert(dom: Dom) {
|
||||
if (this.currentDom) {
|
||||
this.currentDom.content.push(dom);
|
||||
} else {
|
||||
this.dom = dom;
|
||||
}
|
||||
}
|
||||
|
||||
generateExpr(expr: string): string {
|
||||
if (this.type === "block") {
|
||||
const hasChildren = this.children.length;
|
||||
let params = this.data.length ? `[${this.data.join(", ")}]` : hasChildren ? "[]" : "";
|
||||
if (hasChildren) {
|
||||
params += ", [" + this.children.map((c) => c.varName).join(", ") + "]";
|
||||
}
|
||||
return `${this.blockName}(${params})`;
|
||||
} else if (this.type === "list") {
|
||||
return `list(c${this.id})`;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Compiler code
|
||||
// -----------------------------------------------------------------------------
|
||||
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
|
||||
|
||||
interface Context {
|
||||
block: BlockDescription | null;
|
||||
index: number | string;
|
||||
forceNewBlock: boolean;
|
||||
preventRoot?: boolean;
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
class CodeTarget {
|
||||
name: string;
|
||||
signature: string = "";
|
||||
indentLevel = 0;
|
||||
loopLevel = 0;
|
||||
code: string[] = [];
|
||||
hasRoot = false;
|
||||
hasCache = false;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
addLine(line: string, idx?: number) {
|
||||
const prefix = new Array(this.indentLevel + 2).join(" ");
|
||||
if (idx === undefined) {
|
||||
this.code.push(prefix + line);
|
||||
} else {
|
||||
this.code.splice(idx, 0, prefix + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class QWebCompiler {
|
||||
blocks: BlockDescription[] = [];
|
||||
nextId = 1;
|
||||
nextBlockId = 1;
|
||||
shouldProtectScope: boolean = false;
|
||||
shouldDefineAssign: boolean = false;
|
||||
shouldDefineKey0: boolean = false;
|
||||
hasSafeContext: boolean | null = null;
|
||||
hasRef: boolean = false;
|
||||
// hasTCall: boolean = false;
|
||||
isDebug: boolean = false;
|
||||
functions: CodeTarget[] = [];
|
||||
target = new CodeTarget("main");
|
||||
templateName: string;
|
||||
template: string;
|
||||
ast: AST;
|
||||
staticCalls: { id: string; template: string }[] = [];
|
||||
|
||||
constructor(template: string, name?: string) {
|
||||
this.template = template;
|
||||
this.ast = parse(template);
|
||||
// console.warn(this.ast);
|
||||
if (name) {
|
||||
this.templateName = name;
|
||||
} else {
|
||||
if (template.length > 250) {
|
||||
this.templateName = template.slice(0, 250) + "...";
|
||||
} else {
|
||||
this.templateName = template;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compile(): TemplateFunction {
|
||||
const ast = this.ast;
|
||||
this.isDebug = ast.type === ASTType.TDebug;
|
||||
BlockDescription.nextBlockId = 1;
|
||||
BlockDescription.nextDataId = 1;
|
||||
this.compileAST(ast, { block: null, index: 0, forceNewBlock: false, isLast: true });
|
||||
const code = this.generateCode();
|
||||
// console.warn(code);
|
||||
return new Function("bdom, helpers", code) as TemplateFunction;
|
||||
}
|
||||
|
||||
addLine(line: string) {
|
||||
this.target.addLine(line);
|
||||
}
|
||||
|
||||
generateId(prefix: string = ""): string {
|
||||
return `${prefix}${this.nextId++}`;
|
||||
}
|
||||
|
||||
generateBlockName(): string {
|
||||
return `block${this.blocks.length + 1}`;
|
||||
}
|
||||
|
||||
insertAnchor(block: BlockDescription) {
|
||||
const tag = `block-child-${block.children.length}`;
|
||||
const anchor: Dom = { type: DomType.Node, tag, attrs: {}, content: [] };
|
||||
block.insert(anchor);
|
||||
}
|
||||
|
||||
createBlock(
|
||||
parentBlock: BlockDescription | null,
|
||||
type: BlockType,
|
||||
ctx: Context
|
||||
): BlockDescription {
|
||||
const hasRoot = this.target.hasRoot;
|
||||
const block = new BlockDescription(this.target, type);
|
||||
if (!hasRoot && !ctx.preventRoot) {
|
||||
this.target.hasRoot = true;
|
||||
block.isRoot = true;
|
||||
}
|
||||
if (parentBlock) {
|
||||
parentBlock.children.push(block);
|
||||
if (parentBlock.type === "list") {
|
||||
block.parentVar = `c${parentBlock.id}`;
|
||||
}
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
insertBlock(expression: string, block: BlockDescription, ctx: Context): string | null {
|
||||
let id: string | null = null;
|
||||
const blockExpr = block.generateExpr(expression);
|
||||
if (block.parentVar) {
|
||||
this.addLine(
|
||||
`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, key${this.target.loopLevel});`
|
||||
);
|
||||
} else if (block.isRoot && !ctx.preventRoot) {
|
||||
this.addLine(`return ${blockExpr};`);
|
||||
} else {
|
||||
this.addLine(`let ${block.varName} = ${blockExpr};`);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
generateCode(): string {
|
||||
let mainCode = this.target.code;
|
||||
this.target.code = [];
|
||||
this.target.indentLevel = 0;
|
||||
// define blocks and utility functions
|
||||
this.addLine(`let { text, createBlock, list, multi, html, toggler, component } = bdom;`);
|
||||
this.addLine(
|
||||
`let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;`
|
||||
);
|
||||
if (this.shouldDefineAssign) {
|
||||
this.addLine(`let assign = Object.assign;`);
|
||||
}
|
||||
|
||||
for (let { id, template } of this.staticCalls) {
|
||||
this.addLine(`const ${id} = getTemplate(${template});`);
|
||||
}
|
||||
|
||||
// define all blocks
|
||||
if (this.blocks.length) {
|
||||
this.addLine(``);
|
||||
for (let block of this.blocks) {
|
||||
if (block.dom) {
|
||||
this.addLine(`let ${block.blockName} = createBlock(\`${domToString(block.dom)}\`);`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// define all slots
|
||||
for (let fn of this.functions) {
|
||||
this.generateFunctions(fn);
|
||||
}
|
||||
|
||||
// // generate main code
|
||||
this.target.indentLevel = 0;
|
||||
this.addLine(``);
|
||||
this.addLine(`return function template(ctx, node, key = "") {`);
|
||||
if (this.hasRef) {
|
||||
this.addLine(` const refs = ctx.__owl__.refs;`);
|
||||
}
|
||||
if (this.shouldProtectScope) {
|
||||
this.addLine(` ctx = Object.create(ctx);`);
|
||||
}
|
||||
if (this.target.hasCache) {
|
||||
this.addLine(` let cache = ctx.cache || {};`);
|
||||
this.addLine(` let nextCache = ctx.cache = {};`);
|
||||
}
|
||||
// if (this.shouldDefineKey0) {
|
||||
// this.addLine(` let key0;`);
|
||||
// }
|
||||
for (let line of mainCode) {
|
||||
this.addLine(line);
|
||||
}
|
||||
// console.warn(this.target.code.join('\n'))
|
||||
if (!this.target.hasRoot) {
|
||||
throw new Error("missing root block");
|
||||
}
|
||||
this.addLine("}");
|
||||
const code = this.target.code.join("\n");
|
||||
|
||||
if (this.isDebug) {
|
||||
const msg = `[Owl Debug]\n${code}`;
|
||||
console.log(msg);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
generateFunctions(fn: CodeTarget) {
|
||||
this.addLine("");
|
||||
this.addLine(`const ${fn.name} = ${fn.signature}`);
|
||||
if (fn.hasCache) {
|
||||
this.addLine(`let cache = ctx.cache || {};`);
|
||||
this.addLine(`let nextCache = ctx.cache = {};`);
|
||||
}
|
||||
for (let line of fn.code) {
|
||||
this.addLine(line);
|
||||
}
|
||||
this.addLine(`}`);
|
||||
}
|
||||
|
||||
captureExpression(expr: string): string {
|
||||
const tokens = compileExprToArray(expr);
|
||||
const mapping = new Map<string, string>();
|
||||
return tokens
|
||||
.map((tok) => {
|
||||
if (tok.varName) {
|
||||
if (!mapping.has(tok.varName)) {
|
||||
const varId = this.generateId("v");
|
||||
mapping.set(tok.varName, varId);
|
||||
this.addLine(`const ${varId} = ${tok.value};`);
|
||||
}
|
||||
tok.value = mapping.get(tok.varName)!;
|
||||
}
|
||||
return tok.value;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
compileAST(ast: AST, ctx: Context) {
|
||||
switch (ast.type) {
|
||||
case ASTType.Comment:
|
||||
this.compileComment(ast, ctx);
|
||||
break;
|
||||
case ASTType.Text:
|
||||
this.compileText(ast, ctx);
|
||||
break;
|
||||
case ASTType.DomNode:
|
||||
this.compileTDomNode(ast, ctx);
|
||||
break;
|
||||
case ASTType.TEsc:
|
||||
this.compileTEsc(ast, ctx);
|
||||
break;
|
||||
case ASTType.TRaw:
|
||||
this.compileTRaw(ast, ctx);
|
||||
break;
|
||||
case ASTType.TIf:
|
||||
this.compileTIf(ast, ctx);
|
||||
break;
|
||||
case ASTType.TForEach:
|
||||
this.compileTForeach(ast, ctx);
|
||||
break;
|
||||
case ASTType.TKey:
|
||||
this.compileTKey(ast, ctx);
|
||||
break;
|
||||
case ASTType.Multi:
|
||||
this.compileMulti(ast, ctx);
|
||||
break;
|
||||
case ASTType.TCall:
|
||||
this.compileTCall(ast, ctx);
|
||||
break;
|
||||
case ASTType.TCallBlock:
|
||||
this.compileTCallBlock(ast, ctx);
|
||||
break;
|
||||
case ASTType.TSet:
|
||||
this.compileTSet(ast, ctx);
|
||||
break;
|
||||
case ASTType.TComponent:
|
||||
this.compileComponent(ast, ctx);
|
||||
break;
|
||||
case ASTType.TDebug:
|
||||
this.compileDebug(ast, ctx);
|
||||
break;
|
||||
case ASTType.TLog:
|
||||
this.compileLog(ast, ctx);
|
||||
break;
|
||||
case ASTType.TSlot:
|
||||
this.compileTSlot(ast, ctx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
compileDebug(ast: ASTDebug, ctx: Context) {
|
||||
this.addLine(`debugger;`);
|
||||
if (ast.content) {
|
||||
this.compileAST(ast.content, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
compileLog(ast: ASTLog, ctx: Context) {
|
||||
this.addLine(`console.log(${compileExpr(ast.expr)});`);
|
||||
if (ast.content) {
|
||||
this.compileAST(ast.content, ctx);
|
||||
}
|
||||
}
|
||||
compileComment(ast: ASTComment, ctx: Context) {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
if (isNewBlock) {
|
||||
block = this.createBlock(block, "block", ctx);
|
||||
this.blocks.push(block);
|
||||
}
|
||||
const text: Dom = { type: DomType.Comment, value: ast.value };
|
||||
block!.insert(text);
|
||||
if (isNewBlock) {
|
||||
this.insertBlock("", block!, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
compileText(ast: ASTText, ctx: Context) {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
if (!block || forceNewBlock) {
|
||||
block = this.createBlock(block, "text", ctx);
|
||||
this.insertBlock(`text(\`${ast.value}\`)`, block, {
|
||||
...ctx,
|
||||
forceNewBlock: forceNewBlock && !block,
|
||||
});
|
||||
} else {
|
||||
const type = ast.type === ASTType.Text ? DomType.Text : DomType.Comment;
|
||||
const text: Dom = { type, value: ast.value };
|
||||
block.insert(text);
|
||||
}
|
||||
}
|
||||
|
||||
generateHandlerCode(handler: string, event: string = ""): string {
|
||||
let args: string = "";
|
||||
const name: string = handler.replace(/\(.*\)/, function (_args) {
|
||||
args = _args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
const isMethodCall = name.match(FNAMEREGEXP);
|
||||
if (isMethodCall) {
|
||||
let handlerFn: string;
|
||||
if (args) {
|
||||
const argId = this.generateId("arg");
|
||||
this.addLine(`const ${argId} = [${compileExpr(args)}];`);
|
||||
handlerFn = `'${name}', ${argId}`;
|
||||
} else {
|
||||
handlerFn = `'${name}'`;
|
||||
}
|
||||
return `[${event ? `\`${event}\`` + ", " : ""}ctx, ${handlerFn!}]`;
|
||||
} else {
|
||||
let code = this.captureExpression(handler);
|
||||
code = `{const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }}`;
|
||||
let handlerFn = `(e) => ${code}`;
|
||||
if (event) {
|
||||
handlerFn = `[\`${event}\`, ${handlerFn}]`;
|
||||
}
|
||||
return handlerFn;
|
||||
}
|
||||
}
|
||||
|
||||
compileTDomNode(ast: ASTDomNode, ctx: Context) {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
let codeIdx = this.target.code.length;
|
||||
if (isNewBlock) {
|
||||
block = this.createBlock(block, "block", ctx);
|
||||
this.blocks.push(block);
|
||||
}
|
||||
|
||||
// attributes
|
||||
const attrs: { [key: string]: string } = {};
|
||||
for (let key in ast.attrs) {
|
||||
if (key.startsWith("t-attf")) {
|
||||
let expr = interpolate(ast.attrs[key]);
|
||||
const idx = block!.insertData(expr);
|
||||
attrs["block-attribute-" + idx] = key.slice(7);
|
||||
// console.warn('ccc', staticAttrs)
|
||||
} else if (key.startsWith("t-att")) {
|
||||
let expr = compileExpr(ast.attrs[key]);
|
||||
const idx = block!.insertData(expr);
|
||||
if (key === "t-att") {
|
||||
attrs[`block-attributes`] = String(idx);
|
||||
} else {
|
||||
attrs[`block-attribute-${idx}`] = key.slice(6);
|
||||
}
|
||||
} else {
|
||||
attrs[key] = ast.attrs[key];
|
||||
}
|
||||
}
|
||||
|
||||
// event handlers
|
||||
for (let ev in ast.on) {
|
||||
const name = this.generateHandlerCode(ast.on[ev]);
|
||||
const idx = block!.insertData(name);
|
||||
attrs[`block-handler-${idx}`] = ev;
|
||||
}
|
||||
|
||||
// t-ref
|
||||
if (ast.ref) {
|
||||
this.hasRef = true;
|
||||
const isDynamic = INTERP_REGEXP.test(ast.ref);
|
||||
if (isDynamic) {
|
||||
const str = ast.ref.replace(
|
||||
INTERP_REGEXP,
|
||||
(expr) => "${" + this.captureExpression(expr.slice(2, -2)) + "}"
|
||||
);
|
||||
const idx = block!.insertData(`(el) => refs[\`${str}\`] = el`);
|
||||
attrs["block-ref"] = String(idx);
|
||||
} else {
|
||||
const idx = block!.insertData(`(el) => refs[\`${ast.ref}\`] = el`);
|
||||
attrs["block-ref"] = String(idx);
|
||||
}
|
||||
}
|
||||
|
||||
const dom: Dom = { type: DomType.Node, tag: ast.tag, attrs: attrs, content: [] };
|
||||
block!.insert(dom);
|
||||
if (ast.content.length) {
|
||||
const initialDom = block!.currentDom;
|
||||
block!.currentDom = dom;
|
||||
const children = ast.content;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = ast.content[i];
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
index: block!.childNumber,
|
||||
forceNewBlock: false,
|
||||
isLast: ctx.isLast && i === children.length - 1,
|
||||
};
|
||||
this.compileAST(child, subCtx);
|
||||
}
|
||||
block!.currentDom = initialDom;
|
||||
}
|
||||
|
||||
if (isNewBlock) {
|
||||
this.insertBlock(`${block!.blockName}(ddd)`, block!, ctx)!;
|
||||
// may need to rewrite code!
|
||||
if (block!.children.length && block!.hasDynamicChildren) {
|
||||
const code = this.target.code;
|
||||
const children = block!.children.slice();
|
||||
let current = children.shift();
|
||||
for (let i = codeIdx; i < code.length; i++) {
|
||||
if (code[i].trimStart().startsWith(`let ${current!.varName}`)) {
|
||||
code[i] = code[i].replace(`let ${current!.varName}`, current!.varName);
|
||||
current = children.shift();
|
||||
if (!current) break;
|
||||
}
|
||||
}
|
||||
this.target.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileTEsc(ast: ASTTEsc, ctx: Context) {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
let expr: string;
|
||||
if (ast.expr === "0") {
|
||||
expr = `ctx[zero]`;
|
||||
} else {
|
||||
expr = compileExpr(ast.expr);
|
||||
if (ast.defaultValue) {
|
||||
expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
}
|
||||
}
|
||||
if (!block || forceNewBlock) {
|
||||
block = this.createBlock(block, "text", ctx);
|
||||
this.insertBlock(`text(${expr})`, block, { ...ctx, forceNewBlock: forceNewBlock && !block });
|
||||
} else {
|
||||
const idx = block.insertData(expr);
|
||||
const text: Dom = { type: DomType.Node, tag: `block-text-${idx}`, attrs: {}, content: [] };
|
||||
block.insert(text);
|
||||
}
|
||||
}
|
||||
|
||||
compileTRaw(ast: ASTTRaw, ctx: Context) {
|
||||
let { block } = ctx;
|
||||
if (block) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
block = this.createBlock(block, "html", ctx);
|
||||
let expr = ast.expr === "0" ? "ctx[zero]" : compileExpr(ast.expr);
|
||||
if (ast.body) {
|
||||
const nextId = BlockDescription.nextBlockId;
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true };
|
||||
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
|
||||
expr = `withDefault(${expr}, b${nextId})`;
|
||||
}
|
||||
this.insertBlock(`html(${expr})`, block, ctx);
|
||||
}
|
||||
|
||||
compileTIf(ast: ASTTif, ctx: Context, nextNode?: ASTDomNode) {
|
||||
let { block, forceNewBlock, index } = ctx;
|
||||
let currentIndex = index;
|
||||
const codeIdx = this.target.code.length;
|
||||
const isNewBlock = !block || (block.type !== "multi" && forceNewBlock);
|
||||
if (block) {
|
||||
block.hasDynamicChildren = true;
|
||||
}
|
||||
if (!block || (block.type !== "multi" && forceNewBlock)) {
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
}
|
||||
this.addLine(`if (${compileExpr(ast.condition)}) {`);
|
||||
this.target.indentLevel++;
|
||||
this.insertAnchor(block!);
|
||||
const subCtx: Context = { block: block, index: currentIndex, forceNewBlock: true };
|
||||
this.compileAST(ast.content, subCtx);
|
||||
this.target.indentLevel--;
|
||||
if (ast.tElif) {
|
||||
for (let clause of ast.tElif) {
|
||||
this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
|
||||
this.target.indentLevel++;
|
||||
this.insertAnchor(block);
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
index: currentIndex,
|
||||
forceNewBlock: true,
|
||||
};
|
||||
this.compileAST(clause.content, subCtx);
|
||||
this.target.indentLevel--;
|
||||
}
|
||||
}
|
||||
if (ast.tElse) {
|
||||
this.addLine(`} else {`);
|
||||
this.target.indentLevel++;
|
||||
this.insertAnchor(block);
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
index: currentIndex,
|
||||
forceNewBlock: true,
|
||||
};
|
||||
this.compileAST(ast.tElse, subCtx);
|
||||
this.target.indentLevel--;
|
||||
}
|
||||
this.addLine("}");
|
||||
if (isNewBlock) {
|
||||
// note: this part is duplicated from end of compiledomnode:
|
||||
if (block!.children.length) {
|
||||
const code = this.target.code;
|
||||
const children = block!.children.slice();
|
||||
let current = children.shift();
|
||||
for (let i = codeIdx; i < code.length; i++) {
|
||||
if (code[i].trimStart().startsWith(`let ${current!.varName}`)) {
|
||||
code[i] = code[i].replace(`let ${current!.varName}`, current!.varName);
|
||||
current = children.shift();
|
||||
if (!current) break;
|
||||
}
|
||||
}
|
||||
this.target.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
||||
}
|
||||
|
||||
// note: this part is duplicated from end of compilemulti:
|
||||
const args = block!.children.map((c) => c.varName).join(", ");
|
||||
this.insertBlock(`multi([${args}])`, block!, ctx)!;
|
||||
}
|
||||
}
|
||||
|
||||
compileTForeach(ast: ASTTForEach, ctx: Context) {
|
||||
let { block } = ctx;
|
||||
if (block) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
block = this.createBlock(block, "list", ctx);
|
||||
this.target.loopLevel++;
|
||||
const loopVar = `i${this.target.loopLevel}`;
|
||||
this.addLine(`ctx = Object.create(ctx);`);
|
||||
// const cId = this.generateId();
|
||||
const vals = `v${block.id}`;
|
||||
const keys = `k${block.id}`;
|
||||
const l = `l${block.id}`;
|
||||
const c = `c${block.id}`;
|
||||
this.addLine(
|
||||
`const [${keys}, ${vals}, ${l}, ${c}] = prepareList(${compileExpr(ast.collection)});`
|
||||
);
|
||||
this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
|
||||
this.target.indentLevel++;
|
||||
this.addLine(`ctx[\`${ast.elem}\`] = ${vals}[${loopVar}];`);
|
||||
if (!ast.hasNoFirst) {
|
||||
this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
|
||||
}
|
||||
if (!ast.hasNoLast) {
|
||||
this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${vals}.length - 1;`);
|
||||
}
|
||||
if (!ast.hasNoIndex) {
|
||||
this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
|
||||
}
|
||||
if (!ast.hasNoValue) {
|
||||
this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
|
||||
}
|
||||
this.addLine(`let key${this.target.loopLevel} = ${ast.key ? compileExpr(ast.key) : loopVar};`);
|
||||
let id: string;
|
||||
if (ast.memo) {
|
||||
this.target.hasCache = true;
|
||||
this.shouldDefineAssign = true;
|
||||
id = this.generateId();
|
||||
this.addLine(`let memo${id} = ${compileExpr(ast.memo)}`);
|
||||
this.addLine(`let vnode${id} = cache[key${this.target.loopLevel}];`);
|
||||
this.addLine(`if (vnode${id}) {`);
|
||||
this.target.indentLevel++;
|
||||
this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);
|
||||
this.target.indentLevel++;
|
||||
this.addLine(`${c}[${loopVar}] = vnode${id};`);
|
||||
this.addLine(`nextCache[key${this.target.loopLevel}] = vnode${id};`);
|
||||
this.addLine(`continue;`);
|
||||
this.target.indentLevel--;
|
||||
this.addLine("}");
|
||||
this.target.indentLevel--;
|
||||
this.addLine("}");
|
||||
}
|
||||
|
||||
const subCtx: Context = {
|
||||
block: block, //collectionBlock,
|
||||
index: loopVar,
|
||||
forceNewBlock: true,
|
||||
};
|
||||
this.compileAST(ast.body, subCtx);
|
||||
if (!ast.key) {
|
||||
console.warn(
|
||||
`"Directive t-foreach should always be used with a t-key! (in template: '${this.templateName}')"`
|
||||
);
|
||||
}
|
||||
if (ast.memo) {
|
||||
this.addLine(
|
||||
`nextCache[key${this.target.loopLevel}] = assign(${c}[${loopVar}], {memo: memo${id!}});`
|
||||
);
|
||||
}
|
||||
this.target.indentLevel--;
|
||||
this.target.loopLevel--;
|
||||
this.addLine(`}`);
|
||||
if (!ctx.isLast) {
|
||||
this.addLine(`ctx = ctx.__proto__;`);
|
||||
}
|
||||
this.insertBlock("l", block, ctx);
|
||||
}
|
||||
|
||||
compileTKey(ast: ASTTKey, ctx: Context) {
|
||||
this.compileAST(ast.content, ctx);
|
||||
}
|
||||
|
||||
compileMulti(ast: ASTMulti, ctx: Context) {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
const isNewBlock = !block || forceNewBlock;
|
||||
let codeIdx = this.target.code.length;
|
||||
if (isNewBlock) {
|
||||
const n = ast.content.filter((c) => c.type !== ASTType.TSet).length;
|
||||
if (n <= 1) {
|
||||
for (let child of ast.content) {
|
||||
this.compileAST(child, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
}
|
||||
let index = 0;
|
||||
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||||
const child = ast.content[i];
|
||||
const isTSet = child.type === ASTType.TSet;
|
||||
const subCtx: Context = {
|
||||
block: block,
|
||||
index: index,
|
||||
forceNewBlock: !isTSet,
|
||||
preventRoot: ctx.preventRoot,
|
||||
isLast: ctx.isLast && i === l - 1,
|
||||
};
|
||||
this.compileAST(child, subCtx);
|
||||
if (!isTSet) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
if (isNewBlock) {
|
||||
if (block!.hasDynamicChildren) {
|
||||
if (block!.children.length) {
|
||||
const code = this.target.code;
|
||||
const children = block!.children.slice();
|
||||
let current = children.shift();
|
||||
for (let i = codeIdx; i < code.length; i++) {
|
||||
if (code[i].trimStart().startsWith(`let ${current!.varName}`)) {
|
||||
code[i] = code[i].replace(`let ${current!.varName}`, current!.varName);
|
||||
current = children.shift();
|
||||
if (!current) break;
|
||||
}
|
||||
}
|
||||
this.target.addLine(`let ${block!.children.map((c) => c.varName)};`, codeIdx);
|
||||
}
|
||||
}
|
||||
|
||||
const args = block!.children.map((c) => c.varName).join(", ");
|
||||
this.insertBlock(`multi([${args}])`, block!, ctx)!;
|
||||
}
|
||||
}
|
||||
|
||||
compileTCall(ast: ASTTCall, ctx: Context) {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
// this.hasTCall = true;
|
||||
if (ast.body) {
|
||||
this.addLine(`ctx = Object.create(ctx);`);
|
||||
const nextId = BlockDescription.nextBlockId;
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true, preventRoot: true };
|
||||
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
|
||||
if (nextId !== BlockDescription.nextBlockId) {
|
||||
this.addLine(`ctx[zero] = b${nextId};`);
|
||||
}
|
||||
}
|
||||
const isDynamic = INTERP_REGEXP.test(ast.name);
|
||||
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
|
||||
if (block) {
|
||||
if (!forceNewBlock) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
}
|
||||
const key = `key + \`${this.generateComponentKey()}\``;
|
||||
if (isDynamic) {
|
||||
const templateVar = this.generateId("template");
|
||||
this.addLine(`const ${templateVar} = ${subTemplate};`);
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(`call(${templateVar}, ctx, node, ${key})`, block!, {
|
||||
...ctx,
|
||||
forceNewBlock: !block,
|
||||
});
|
||||
} else {
|
||||
const id = this.generateId(`callTemplate_`);
|
||||
this.staticCalls.push({ id, template: subTemplate });
|
||||
// console.warn('coucoup', this.target.hasRoot)
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(`${id}(ctx, node, ${key})`, block!, { ...ctx, forceNewBlock: !block });
|
||||
}
|
||||
if (ast.body && !ctx.isLast) {
|
||||
this.addLine(`ctx = ctx.__proto__;`);
|
||||
}
|
||||
}
|
||||
|
||||
compileTCallBlock(ast: ASTTCallBlock, ctx: Context) {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
if (block) {
|
||||
if (!forceNewBlock) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
}
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });
|
||||
}
|
||||
|
||||
compileTSet(ast: ASTTSet, ctx: Context) {
|
||||
this.shouldProtectScope = true;
|
||||
const expr = ast.value ? compileExpr(ast.value || "") : "null";
|
||||
if (ast.body) {
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true };
|
||||
const nextId = `b${BlockDescription.nextBlockId}`;
|
||||
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
|
||||
const value = ast.value ? (nextId ? `withDefault(${expr}, ${nextId})` : expr) : nextId;
|
||||
this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
|
||||
} else {
|
||||
let value: string;
|
||||
if (ast.defaultValue) {
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
} else {
|
||||
value = `\`${ast.defaultValue}\``;
|
||||
}
|
||||
} else {
|
||||
value = expr;
|
||||
}
|
||||
this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
|
||||
}
|
||||
}
|
||||
|
||||
generateComponentKey() {
|
||||
const parts = [this.generateId("__")];
|
||||
for (let i = 0; i < this.target.loopLevel; i++) {
|
||||
parts.push(`\${key${i + 1}}`);
|
||||
}
|
||||
return parts.join("__");
|
||||
}
|
||||
|
||||
compileComponent(ast: ASTComponent, ctx: Context) {
|
||||
let { block } = ctx;
|
||||
let extraArgs: { [key: string]: string } = {};
|
||||
|
||||
// props
|
||||
const props: string[] = [];
|
||||
for (let p in ast.props) {
|
||||
props.push(`${p}: ${compileExpr(ast.props[p]) || undefined}`);
|
||||
}
|
||||
const propString = `{${props.join(",")}}`;
|
||||
|
||||
// cmap key
|
||||
const key = this.generateComponentKey();
|
||||
let expr: string;
|
||||
if (ast.isDynamic) {
|
||||
expr = this.generateId("Comp");
|
||||
this.addLine(`let ${expr} = ${compileExpr(ast.name)};`);
|
||||
} else {
|
||||
expr = `\`${ast.name}\``;
|
||||
}
|
||||
let blockArgs = `${expr}, ${propString}, key + \`${key}\`, node, ctx`;
|
||||
|
||||
// slots
|
||||
const hasSlot = !!Object.keys(ast.slots).length;
|
||||
let slotDef: string;
|
||||
if (hasSlot) {
|
||||
if (this.hasSafeContext === null) {
|
||||
this.hasSafeContext = !this.template.includes("t-set") && !this.template.includes("t-call");
|
||||
}
|
||||
let ctxStr = "ctx";
|
||||
if (this.target.loopLevel || !this.hasSafeContext) {
|
||||
ctxStr = this.generateId("ctx");
|
||||
this.addLine(`const ${ctxStr} = capture(ctx);`);
|
||||
}
|
||||
let slotStr: string[] = [];
|
||||
const initialTarget = this.target;
|
||||
for (let slotName in ast.slots) {
|
||||
let name = this.generateId("slot");
|
||||
const slot = new CodeTarget(name);
|
||||
slot.signature = "ctx => (node, key) => {";
|
||||
this.functions.push(slot);
|
||||
this.target = slot;
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true };
|
||||
this.compileAST(ast.slots[slotName], subCtx);
|
||||
if (this.hasRef) {
|
||||
slot.signature = "ctx => node => {";
|
||||
slot.code.unshift(` const refs = ctx.__owl__.refs`);
|
||||
slotStr.push(`'${slotName}': ${name}(${ctxStr})`);
|
||||
} else {
|
||||
slotStr.push(`'${slotName}': ${name}(${ctxStr})`);
|
||||
}
|
||||
}
|
||||
this.target = initialTarget;
|
||||
slotDef = `{${slotStr.join(", ")}}`;
|
||||
extraArgs.slots = slotDef;
|
||||
}
|
||||
|
||||
// handlers
|
||||
const hasHandlers = Object.keys(ast.handlers).length;
|
||||
if (hasHandlers) {
|
||||
const vars = Object.keys(ast.handlers).map((ev) => {
|
||||
let id = this.generateId("h");
|
||||
this.addLine(`let ${id} = ${this.generateHandlerCode(ast.handlers[ev], ev)};`);
|
||||
return id;
|
||||
});
|
||||
extraArgs.handlers = `[${vars}]`;
|
||||
}
|
||||
|
||||
if (block && ctx.forceNewBlock === false) {
|
||||
// todo: check the forcenewblock condition
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
let blockExpr = `component(${blockArgs})`;
|
||||
if (Object.keys(extraArgs).length) {
|
||||
this.shouldDefineAssign = true;
|
||||
const content = Object.keys(extraArgs).map((k) => `${k}: ${extraArgs[k]}`);
|
||||
blockExpr = `assign(${blockExpr}, {${content.join(", ")}})`;
|
||||
}
|
||||
if (ast.isDynamic) {
|
||||
blockExpr = `toggler(${expr}, ${blockExpr})`;
|
||||
}
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(blockExpr, block, ctx);
|
||||
}
|
||||
|
||||
compileTSlot(ast: ASTSlot, ctx: Context) {
|
||||
let { block } = ctx;
|
||||
let blockString: string;
|
||||
let slotName;
|
||||
let dynamic = false;
|
||||
if (ast.name.match(INTERP_REGEXP)) {
|
||||
dynamic = true;
|
||||
slotName = interpolate(ast.name);
|
||||
} else {
|
||||
slotName = "'" + ast.name + "'";
|
||||
}
|
||||
if (ast.defaultContent) {
|
||||
let name = this.generateId("defaultSlot");
|
||||
const slot = new CodeTarget(name);
|
||||
slot.signature = "ctx => {";
|
||||
this.functions.push(slot);
|
||||
const initialTarget = this.target;
|
||||
const subCtx: Context = { block: null, index: 0, forceNewBlock: true };
|
||||
this.target = slot;
|
||||
this.compileAST(ast.defaultContent, subCtx);
|
||||
this.target = initialTarget;
|
||||
blockString = `callSlot(ctx, node, key, ${slotName}, ${name}, ${dynamic})`;
|
||||
} else {
|
||||
if (dynamic) {
|
||||
let name = this.generateId("slot");
|
||||
this.addLine(`const ${name} = ${slotName};`);
|
||||
blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}))`;
|
||||
} else {
|
||||
blockString = `callSlot(ctx, node, key, ${slotName})`;
|
||||
}
|
||||
}
|
||||
if (block) {
|
||||
this.insertAnchor(block);
|
||||
}
|
||||
block = this.createBlock(block, "multi", ctx);
|
||||
this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
export const enum DomType {
|
||||
Text,
|
||||
Comment,
|
||||
Node,
|
||||
}
|
||||
|
||||
export interface DomText {
|
||||
type: DomType.Text;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface DomComment {
|
||||
type: DomType.Comment;
|
||||
value: string;
|
||||
}
|
||||
export interface DomNode {
|
||||
type: DomType.Node;
|
||||
tag: string;
|
||||
attrs: { [key: string]: string };
|
||||
content: Dom[];
|
||||
}
|
||||
|
||||
export type Dom = DomText | DomComment | DomNode;
|
||||
|
||||
function escape(str: string): string {
|
||||
const p = document.createElement("p");
|
||||
p.textContent = str;
|
||||
return p.innerHTML;
|
||||
}
|
||||
|
||||
export function domToString(dom: Dom): string {
|
||||
switch (dom.type) {
|
||||
case DomType.Text:
|
||||
return escape(dom.value);
|
||||
case DomType.Comment:
|
||||
return `<!--${dom.value}-->`;
|
||||
case DomType.Node:
|
||||
const content = dom.content.map(domToString).join("");
|
||||
const attrs: string[] = [];
|
||||
for (let [key, value] of Object.entries(dom.attrs)) {
|
||||
if (!(key === "class" && value === "")) {
|
||||
attrs.push(`${key}="${escape(value)}"`);
|
||||
}
|
||||
}
|
||||
if (content) {
|
||||
return `<${dom.tag}${attrs.length ? " " + attrs.join(" ") : ""}>${content}</${dom.tag}>`;
|
||||
} else {
|
||||
return `<${dom.tag}${attrs.length ? " " + attrs.join(" ") : ""}/>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isProp(tag: string, key: string): boolean {
|
||||
switch (tag) {
|
||||
case "input":
|
||||
return (
|
||||
key === "checked" ||
|
||||
key === "indeterminate" ||
|
||||
key === "value" ||
|
||||
key === "readonly" ||
|
||||
key === "disabled"
|
||||
);
|
||||
case "option":
|
||||
return key === "selected" || key === "disabled";
|
||||
case "textarea":
|
||||
return key === "readonly" || key === "disabled";
|
||||
break;
|
||||
case "button":
|
||||
case "select":
|
||||
case "optgroup":
|
||||
return key === "disabled";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
import { STATUS } from "../component/component";
|
||||
import { VNode } from "../vdom/index";
|
||||
import { INTERP_REGEXP } from "./compilation_context";
|
||||
import { QWeb } from "./qweb";
|
||||
import { browser } from "../browser";
|
||||
|
||||
/**
|
||||
* Owl QWeb Extensions
|
||||
*
|
||||
* This file contains the implementation of non standard QWeb directives, added
|
||||
* by Owl and that will only work on Owl projects:
|
||||
*
|
||||
* - t-on
|
||||
* - t-ref
|
||||
* - t-transition
|
||||
* - t-mounted
|
||||
* - t-slot
|
||||
* - t-model
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-on
|
||||
//------------------------------------------------------------------------------
|
||||
// these are pieces of code that will be injected into the event handler if
|
||||
// modifiers are specified
|
||||
export const MODS_CODE = {
|
||||
prevent: "e.preventDefault();",
|
||||
self: "if (e.target !== this.elm) {return}",
|
||||
stop: "e.stopPropagation();",
|
||||
};
|
||||
|
||||
interface HandlerInfo {
|
||||
event: string;
|
||||
handler: string;
|
||||
}
|
||||
|
||||
const FNAMEREGEXP = /^[$A-Z_][0-9A-Z_$]*$/i;
|
||||
|
||||
export function makeHandlerCode(
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
putInCache: boolean,
|
||||
modcodes = MODS_CODE
|
||||
): HandlerInfo {
|
||||
let [event, ...mods] = fullName.slice(5).split(".");
|
||||
if (mods.includes("capture")) {
|
||||
event = "!" + event;
|
||||
}
|
||||
if (!event) {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
let code: string;
|
||||
// check if it is a method with no args, a method with args or an expression
|
||||
let args: string = "";
|
||||
const name: string = value.replace(/\(.*\)/, function (_args) {
|
||||
args = _args.slice(1, -1);
|
||||
return "";
|
||||
});
|
||||
const isMethodCall = name.match(FNAMEREGEXP);
|
||||
|
||||
// then generate code
|
||||
if (isMethodCall) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
const comp = `utils.getComponent(context)`;
|
||||
if (args) {
|
||||
const argId = ctx.generateID();
|
||||
ctx.addLine(`let args${argId} = [${ctx.formatExpression(args)}];`);
|
||||
code = `${comp}['${name}'](...args${argId}, e);`;
|
||||
putInCache = false;
|
||||
} else {
|
||||
code = `${comp}['${name}'](e);`;
|
||||
}
|
||||
} else {
|
||||
// if we get here, then it is an expression
|
||||
// we need to capture every variable in it
|
||||
putInCache = false;
|
||||
code = ctx.captureExpression(value);
|
||||
code = `const res = (() => { return ${code} })(); if (typeof res === 'function') { res(e) }`;
|
||||
}
|
||||
const modCode = mods.map((mod) => modcodes[mod]).join("");
|
||||
let handler = `function (e) {if (context.__owl__.status === ${STATUS.DESTROYED}){return}${modCode}${code}}`;
|
||||
if (putInCache) {
|
||||
const key = ctx.generateTemplateKey(event);
|
||||
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || ${handler};`);
|
||||
handler = `extra.handlers[${key}]`;
|
||||
}
|
||||
return { event, handler };
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "on",
|
||||
priority: 90,
|
||||
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||
const { event, handler } = makeHandlerCode(ctx, fullName, value, true);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = ${handler};`);
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-ref
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "ref",
|
||||
priority: 95,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
ctx.rootContext.shouldDefineRefs = true;
|
||||
const refKey = `ref${ctx.generateID()}`;
|
||||
ctx.addLine(`const ${refKey} = ${ctx.interpolate(value)};`);
|
||||
addNodeHook("create", `context.__owl__.refs[${refKey}] = n.elm;`);
|
||||
addNodeHook("destroy", `delete context.__owl__.refs[${refKey}];`);
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-transition
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.nextFrame = function (cb: () => void) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(cb));
|
||||
};
|
||||
|
||||
QWeb.utils.transitionInsert = function (vn: VNode, name: string) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
// remove potential duplicated vnode that is currently being removed, to
|
||||
// prevent from having twice the same node in the DOM during an animation
|
||||
const dup = elm.parentElement && elm.parentElement!.querySelector(`*[data-owl-key='${vn.key}']`);
|
||||
if (dup) {
|
||||
dup.remove();
|
||||
}
|
||||
|
||||
elm.classList.add(name + "-enter");
|
||||
elm.classList.add(name + "-enter-active");
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
const finalize = () => {
|
||||
elm.classList.remove(name + "-enter-active");
|
||||
elm.classList.remove(name + "-enter-to");
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-enter");
|
||||
elm.classList.add(name + "-enter-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
QWeb.utils.transitionRemove = function (vn: VNode, name: string, rm: () => void) {
|
||||
const elm = <HTMLElement>vn.elm;
|
||||
elm.setAttribute("data-owl-key", vn.key!);
|
||||
|
||||
elm.classList.add(name + "-leave");
|
||||
elm.classList.add(name + "-leave-active");
|
||||
const finalize = () => {
|
||||
if (!elm.classList.contains(name + "-leave-active")) {
|
||||
return;
|
||||
}
|
||||
elm.classList.remove(name + "-leave-active");
|
||||
elm.classList.remove(name + "-leave-to");
|
||||
rm();
|
||||
};
|
||||
this.nextFrame(() => {
|
||||
elm.classList.remove(name + "-leave");
|
||||
elm.classList.add(name + "-leave-to");
|
||||
whenTransitionEnd(elm, finalize);
|
||||
});
|
||||
};
|
||||
|
||||
function getTimeout(delays: Array<string>, durations: Array<string>): number {
|
||||
/* istanbul ignore next */
|
||||
while (delays.length < durations.length) {
|
||||
delays = delays.concat(delays);
|
||||
}
|
||||
|
||||
return Math.max.apply(
|
||||
null,
|
||||
durations.map((d, i) => {
|
||||
return toMs(d) + toMs(delays[i]);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
|
||||
// in a locale-dependent way, using a comma instead of a dot.
|
||||
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
|
||||
// as a floor function) causing unexpected behaviors
|
||||
function toMs(s: string): number {
|
||||
return Number(s.slice(0, -1).replace(",", ".")) * 1000;
|
||||
}
|
||||
|
||||
function whenTransitionEnd(elm: HTMLElement, cb) {
|
||||
if (!elm.parentNode) {
|
||||
// if we get here, this means that the element was removed for some other
|
||||
// reasons, and in that case, we don't want to work on animation since nothing
|
||||
// will be displayed anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = window.getComputedStyle(elm);
|
||||
const delays: Array<string> = (styles.transitionDelay || "").split(", ");
|
||||
const durations: Array<string> = (styles.transitionDuration || "").split(", ");
|
||||
const timeout: number = getTimeout(delays, durations);
|
||||
if (timeout > 0) {
|
||||
const transitionEndCB = () => {
|
||||
if (!elm.parentNode) return;
|
||||
cb();
|
||||
browser.clearTimeout(fallbackTimeout);
|
||||
elm.removeEventListener("transitionend", transitionEndCB);
|
||||
};
|
||||
elm.addEventListener("transitionend", transitionEndCB, { once: true });
|
||||
const fallbackTimeout = browser.setTimeout(transitionEndCB, timeout + 1);
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "transition",
|
||||
priority: 96,
|
||||
atNodeCreation({ ctx, value, addNodeHook }) {
|
||||
if (!QWeb.enableTransitions) {
|
||||
return;
|
||||
}
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
let name = value;
|
||||
const hooks = {
|
||||
insert: `utils.transitionInsert(vn, '${name}');`,
|
||||
remove: `utils.transitionRemove(vn, '${name}', rm);`,
|
||||
};
|
||||
for (let hookName in hooks) {
|
||||
addNodeHook(hookName, hooks[hookName]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-slot
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "slot",
|
||||
priority: 80,
|
||||
atNodeEncounter({ ctx, value, node, qweb }): boolean {
|
||||
const slotKey = ctx.generateID();
|
||||
const valueExpr = value.match(INTERP_REGEXP) ? ctx.interpolate(value) : `'${value}'`;
|
||||
ctx.addLine(
|
||||
`const slot${slotKey} = this.constructor.slots[context.__owl__.slotId + '_' + ${valueExpr}];`
|
||||
);
|
||||
ctx.addIf(`slot${slotKey}`);
|
||||
let parentNode = `c${ctx.parentNode}`;
|
||||
if (!ctx.parentNode) {
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
parentNode = `children${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${parentNode}= []`);
|
||||
ctx.addLine(`result = {}`);
|
||||
}
|
||||
ctx.addLine(
|
||||
`slot${slotKey}.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: ${parentNode}, parent: extra.parent || context}));`
|
||||
);
|
||||
if (!ctx.parentNode) {
|
||||
ctx.addLine(`utils.defineProxy(result, ${parentNode}[0]);`);
|
||||
}
|
||||
if (node.hasChildNodes()) {
|
||||
ctx.addElse();
|
||||
const nodeCopy = <Element>node.cloneNode(true);
|
||||
nodeCopy.removeAttribute("t-slot");
|
||||
qweb._compileNode(nodeCopy, ctx);
|
||||
}
|
||||
ctx.closeIf();
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-model
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.utils.toNumber = function (val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
|
||||
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
|
||||
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
|
||||
|
||||
QWeb.addDirective({
|
||||
name: "model",
|
||||
priority: 42,
|
||||
atNodeCreation({ ctx, nodeID, value, node, fullName, addNodeHook }) {
|
||||
const type = node.getAttribute("type");
|
||||
let handler;
|
||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||
|
||||
// First step: we need to understand the structure of the expression, and
|
||||
// from it, extract a base expression (that we can capture, which is
|
||||
// important because it will be used in a handler later) and a formatted
|
||||
// expression (which uses the captured base expression)
|
||||
//
|
||||
// Also, we support 2 kinds of values: some.expr.value or some.expr[value]
|
||||
// For the first one, we have:
|
||||
// - base expression = scope[some].expr
|
||||
// - expression = exprX.value (where exprX is the var that captures the base expr)
|
||||
// and for the expression with brackets:
|
||||
// - base expression = scope[some].expr
|
||||
// - expression = exprX[keyX] (where exprX is the var that captures the base expr
|
||||
// and keyX captures scope[value])
|
||||
let expr: string;
|
||||
let baseExpr: string;
|
||||
|
||||
if (hasDotAtTheEnd.test(value)) {
|
||||
// we manage the case where the expr has a dot: some.expr.value
|
||||
const index = value.lastIndexOf(".");
|
||||
baseExpr = value.slice(0, index);
|
||||
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
|
||||
expr = `expr${nodeID}${value.slice(index)}`;
|
||||
} else if (hasBracketsAtTheEnd.test(value)) {
|
||||
// we manage here the case where the expr ends in a bracket expression:
|
||||
// some.expr[value]
|
||||
const index = value.lastIndexOf("[");
|
||||
baseExpr = value.slice(0, index);
|
||||
ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`);
|
||||
let exprKey = value.trimRight().slice(index + 1, -1);
|
||||
ctx.addLine(`let exprKey${nodeID} = ${ctx.formatExpression(exprKey)};`);
|
||||
expr = `expr${nodeID}[exprKey${nodeID}]`;
|
||||
} else {
|
||||
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
||||
}
|
||||
|
||||
const key = ctx.generateTemplateKey();
|
||||
if (node.tagName === "select") {
|
||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||
addNodeHook("create", `n.elm.value=${expr};`);
|
||||
event = "change";
|
||||
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||
} else if (type === "checkbox") {
|
||||
ctx.addLine(`p${nodeID}.props = {checked: ${expr}};`);
|
||||
handler = `(ev) => {${expr} = ev.target.checked}`;
|
||||
} else if (type === "radio") {
|
||||
const nodeValue = node.getAttribute("value")!;
|
||||
ctx.addLine(`p${nodeID}.props = {checked:${expr} === '${nodeValue}'};`);
|
||||
handler = `(ev) => {${expr} = ev.target.value}`;
|
||||
event = "click";
|
||||
} else {
|
||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||
const trimCode = fullName.includes(".trim") ? ".trim()" : "";
|
||||
let valueCode = `ev.target.value${trimCode}`;
|
||||
if (fullName.includes(".number")) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
valueCode = `utils.toNumber(${valueCode})`;
|
||||
}
|
||||
handler = `(ev) => {${expr} = ${valueCode}}`;
|
||||
}
|
||||
ctx.addLine(`extra.handlers[${key}] = extra.handlers[${key}] || (${handler});`);
|
||||
ctx.addLine(`p${nodeID}.on['${event}'] = extra.handlers[${key}];`);
|
||||
},
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// t-key
|
||||
//------------------------------------------------------------------------------
|
||||
QWeb.addDirective({
|
||||
name: "key",
|
||||
priority: 45,
|
||||
atNodeEncounter({ ctx, value, node }) {
|
||||
if (ctx.loopNumber === 0) {
|
||||
ctx.keyStack.push(ctx.rootContext.hasKey0);
|
||||
ctx.rootContext.hasKey0 = true;
|
||||
}
|
||||
ctx.addLine("{");
|
||||
ctx.indent();
|
||||
ctx.addLine(`let key${ctx.loopNumber} = ${ctx.formatExpression(value)};`);
|
||||
},
|
||||
finalize({ ctx }) {
|
||||
ctx.dedent();
|
||||
ctx.addLine("}");
|
||||
if (ctx.loopNumber === 0) {
|
||||
ctx.rootContext.hasKey0 = ctx.keyStack.pop() as boolean;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
import "./base_directives";
|
||||
import "./extensions";
|
||||
|
||||
export { CompiledTemplate, QWeb } from "./qweb";
|
||||
@@ -30,7 +30,7 @@ const RESERVED_WORDS =
|
||||
","
|
||||
);
|
||||
|
||||
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
|
||||
const WORD_REPLACEMENT: { [key: string]: string } = Object.assign(Object.create(null), {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
@@ -70,7 +70,6 @@ interface Token {
|
||||
size?: number;
|
||||
varName?: string;
|
||||
replace?: Function;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
|
||||
@@ -119,7 +118,7 @@ let tokenizeString: Tokenizer = function (expr) {
|
||||
return {
|
||||
type: "TEMPLATE_STRING",
|
||||
value: s,
|
||||
replace(replacer) {
|
||||
replace(replacer: any) {
|
||||
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
|
||||
return "${" + replacer(group) + "}";
|
||||
});
|
||||
@@ -225,8 +224,9 @@ export function tokenize(expr: string): Token[] {
|
||||
// Expression "evaluator"
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
|
||||
const isRightSeparator = (token) =>
|
||||
const isLeftSeparator = (token: Token) =>
|
||||
token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
|
||||
const isRightSeparator = (token: Token) =>
|
||||
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
|
||||
|
||||
/**
|
||||
@@ -254,11 +254,9 @@ const isRightSeparator = (token) =>
|
||||
* 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
|
||||
*/
|
||||
export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] {
|
||||
export function compileExprToArray(expr: string): Token[] {
|
||||
const localVars = new Set<string>();
|
||||
scope = Object.create(scope);
|
||||
const tokens = tokenize(expr);
|
||||
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
|
||||
@@ -301,7 +299,7 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
|
||||
}
|
||||
}
|
||||
if (token.type === "TEMPLATE_STRING") {
|
||||
token.value = token.replace((expr) => compileExpr(expr, scope));
|
||||
token.value = token.replace!((expr: any) => compileExpr(expr));
|
||||
}
|
||||
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
|
||||
if (token.type === "RIGHT_PAREN") {
|
||||
@@ -309,40 +307,42 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar
|
||||
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 };
|
||||
localVars.add(tokens[j].value);
|
||||
localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
|
||||
}
|
||||
j--;
|
||||
}
|
||||
} else {
|
||||
scope[token.value] = { id: token.value, expr: token.value };
|
||||
localVars.add(token.value);
|
||||
localVars.add(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 {
|
||||
if (!localVars.has(token.value)) {
|
||||
token.originalValue = token.value;
|
||||
token.value = `scope['${token.value}']`;
|
||||
token.value = `ctx['${token.value}']`;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
// Mark all variables that have been used locally.
|
||||
// This assumes the expression has only one scope (incorrect but "good enough for now")
|
||||
for (const token of tokens) {
|
||||
if (token.type === "SYMBOL" && localVars.has(token.value)) {
|
||||
token.isLocal = true;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function compileExpr(expr: string, scope: { [key: string]: QWebVar }): string {
|
||||
return compileExprToArray(expr, scope)
|
||||
export function compileExpr(expr: string): string {
|
||||
return compileExprToArray(expr)
|
||||
.map((t) => t.value)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||
const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
|
||||
|
||||
export function interpolate(s: string): string {
|
||||
let matches = s.match(INTERP_REGEXP);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${compileExpr(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
|
||||
return "`" + r + "`";
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AST Type definition
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const enum ASTType {
|
||||
Text,
|
||||
Comment,
|
||||
DomNode,
|
||||
Multi,
|
||||
TEsc,
|
||||
TIf,
|
||||
TSet,
|
||||
TCall,
|
||||
TRaw,
|
||||
TForEach,
|
||||
TKey,
|
||||
TComponent,
|
||||
TDebug,
|
||||
TLog,
|
||||
TSlot,
|
||||
TCallBlock,
|
||||
}
|
||||
|
||||
export interface ASTText {
|
||||
type: ASTType.Text;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ASTComment {
|
||||
type: ASTType.Comment;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ASTDomNode {
|
||||
type: ASTType.DomNode;
|
||||
tag: string;
|
||||
attrs: { [key: string]: string };
|
||||
content: AST[];
|
||||
ref: string | null;
|
||||
on: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface ASTMulti {
|
||||
type: ASTType.Multi;
|
||||
content: AST[];
|
||||
}
|
||||
|
||||
export interface ASTTEsc {
|
||||
type: ASTType.TEsc;
|
||||
expr: string;
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
export interface ASTTRaw {
|
||||
type: ASTType.TRaw;
|
||||
expr: string;
|
||||
body: AST[] | null;
|
||||
}
|
||||
|
||||
export interface ASTTif {
|
||||
type: ASTType.TIf;
|
||||
condition: string;
|
||||
content: AST;
|
||||
tElif: { condition: string; content: AST }[] | null;
|
||||
tElse: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTSet {
|
||||
type: ASTType.TSet;
|
||||
name: string;
|
||||
value: string | null; // value defined in attribute
|
||||
defaultValue: string | null; // value defined in body, if text
|
||||
body: AST[] | null; // content of body if not text
|
||||
}
|
||||
|
||||
export interface ASTTForEach {
|
||||
type: ASTType.TForEach;
|
||||
collection: string;
|
||||
elem: string;
|
||||
key: string | null;
|
||||
body: AST;
|
||||
memo: string;
|
||||
isOnlyChild: boolean;
|
||||
hasNoComponent: boolean;
|
||||
hasNoFirst: boolean;
|
||||
hasNoLast: boolean;
|
||||
hasNoIndex: boolean;
|
||||
hasNoValue: boolean;
|
||||
}
|
||||
|
||||
export interface ASTTKey {
|
||||
type: ASTType.TKey;
|
||||
expr: string;
|
||||
content: AST;
|
||||
}
|
||||
|
||||
export interface ASTTCall {
|
||||
type: ASTType.TCall;
|
||||
name: string;
|
||||
body: AST[] | null;
|
||||
}
|
||||
|
||||
export interface ASTComponent {
|
||||
type: ASTType.TComponent;
|
||||
name: string;
|
||||
isDynamic: boolean;
|
||||
props: { [name: string]: string };
|
||||
handlers: { [event: string]: string };
|
||||
slots: { [name: string]: AST };
|
||||
}
|
||||
|
||||
export interface ASTSlot {
|
||||
type: ASTType.TSlot;
|
||||
name: string;
|
||||
defaultContent: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTCallBlock {
|
||||
type: ASTType.TCallBlock;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ASTDebug {
|
||||
type: ASTType.TDebug;
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTLog {
|
||||
type: ASTType.TLog;
|
||||
expr: string;
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export type AST =
|
||||
| ASTText
|
||||
| ASTComment
|
||||
| ASTDomNode
|
||||
| ASTMulti
|
||||
| ASTTEsc
|
||||
| ASTTif
|
||||
| ASTTSet
|
||||
| ASTTCall
|
||||
| ASTTRaw
|
||||
| ASTTForEach
|
||||
| ASTTKey
|
||||
| ASTComponent
|
||||
| ASTSlot
|
||||
| ASTTCallBlock
|
||||
| ASTLog
|
||||
| ASTDebug;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Parser
|
||||
// -----------------------------------------------------------------------------
|
||||
interface ParsingContext {
|
||||
inPreTag: boolean;
|
||||
}
|
||||
|
||||
export function parse(xml: string): AST {
|
||||
const template = `<t>${xml}</t>`;
|
||||
const doc = parseXML(template);
|
||||
const ctx = { inPreTag: false };
|
||||
const ast = parseNode(doc.firstChild!, ctx);
|
||||
if (!ast) {
|
||||
return { type: ASTType.Text, value: "" };
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
|
||||
if (!(node instanceof Element)) {
|
||||
return parseTextCommentNode(node, ctx);
|
||||
}
|
||||
return (
|
||||
parseTDebugLog(node, ctx) ||
|
||||
parseTForEach(node, ctx) ||
|
||||
parseTIf(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseTRawNode(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
parseTSetNode(node, ctx) ||
|
||||
parseTNode(node, ctx)
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// <t /> tag
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.tagName !== "t") {
|
||||
return null;
|
||||
}
|
||||
const children: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const ast = parseNode(child, ctx);
|
||||
if (ast) {
|
||||
children.push(ast);
|
||||
}
|
||||
}
|
||||
switch (children.length) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return children[0];
|
||||
default:
|
||||
return {
|
||||
type: ASTType.Multi,
|
||||
content: children,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Text and Comment Nodes
|
||||
// -----------------------------------------------------------------------------
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
|
||||
if (node.nodeType === 3) {
|
||||
let value = node.textContent || "";
|
||||
if (!ctx.inPreTag) {
|
||||
if (lineBreakRE.test(value) && !value.trim()) {
|
||||
return null;
|
||||
}
|
||||
value = value.replace(whitespaceRE, " ");
|
||||
}
|
||||
|
||||
return { type: ASTType.Text, value };
|
||||
} else if (node.nodeType === 8) {
|
||||
return { type: ASTType.Comment, value: node.textContent || "" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// debugging
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.hasAttribute("t-debug")) {
|
||||
node.removeAttribute("t-debug");
|
||||
return {
|
||||
type: ASTType.TDebug,
|
||||
content: parseNode(node, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
if (node.hasAttribute("t-log")) {
|
||||
const expr = node.getAttribute("t-log")!;
|
||||
node.removeAttribute("t-log");
|
||||
return {
|
||||
type: ASTType.TLog,
|
||||
expr,
|
||||
content: parseNode(node, ctx),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Regular dom node
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.tagName === "t") {
|
||||
return null;
|
||||
}
|
||||
const children: AST[] = [];
|
||||
if (node.tagName === "pre") {
|
||||
ctx = { inPreTag: true };
|
||||
}
|
||||
let ref = null;
|
||||
if (node.hasAttribute("t-ref")) {
|
||||
ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
}
|
||||
|
||||
for (let child of node.childNodes) {
|
||||
const ast = parseNode(child, ctx);
|
||||
if (ast) {
|
||||
children.push(ast);
|
||||
}
|
||||
}
|
||||
|
||||
const attrs: ASTDomNode["attrs"] = {};
|
||||
const on: ASTDomNode["on"] = {};
|
||||
|
||||
for (let attr of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(attr)!;
|
||||
if (attr.startsWith("t-on")) {
|
||||
if (attr === "t-on") {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
on[attr.slice(5)] = value;
|
||||
} else {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new Error(`Unknown QWeb directive: '${attr}'`);
|
||||
}
|
||||
attrs[attr] = value;
|
||||
}
|
||||
}
|
||||
if (children.length === 1 && children[0].type === ASTType.TForEach) {
|
||||
children[0].isOnlyChild = true;
|
||||
}
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: node.tagName,
|
||||
attrs,
|
||||
on,
|
||||
ref,
|
||||
content: children,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-esc
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-esc")) {
|
||||
return null;
|
||||
}
|
||||
const escValue = node.getAttribute("t-esc")!;
|
||||
node.removeAttribute("t-esc");
|
||||
const tesc: AST = {
|
||||
type: ASTType.TEsc,
|
||||
expr: escValue,
|
||||
defaultValue: node.textContent || "",
|
||||
};
|
||||
let ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
const ast = parseNode(node, ctx);
|
||||
if (!ast) {
|
||||
return tesc;
|
||||
}
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: ast.tag,
|
||||
attrs: ast.attrs,
|
||||
on: ast.on,
|
||||
ref,
|
||||
content: [tesc],
|
||||
};
|
||||
}
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: tesc },
|
||||
};
|
||||
}
|
||||
return tesc;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-raw
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTRawNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-raw")) {
|
||||
return null;
|
||||
}
|
||||
const expr = node.getAttribute("t-raw")!;
|
||||
node.removeAttribute("t-raw");
|
||||
|
||||
const tRaw: AST = { type: ASTType.TRaw, expr, body: null };
|
||||
const ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
const ast = parseNode(node, ctx);
|
||||
if (!ast) {
|
||||
return tRaw;
|
||||
}
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
tRaw.body = ast.content.length ? ast.content : null;
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: ast.tag,
|
||||
attrs: ast.attrs,
|
||||
on: ast.on,
|
||||
ref,
|
||||
content: [tRaw],
|
||||
};
|
||||
}
|
||||
|
||||
return tRaw;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-foreach and t-key
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-foreach")) {
|
||||
return null;
|
||||
}
|
||||
const html = node.outerHTML;
|
||||
const collection = node.getAttribute("t-foreach")!;
|
||||
node.removeAttribute("t-foreach");
|
||||
const elem = node.getAttribute("t-as") || "";
|
||||
node.removeAttribute("t-as");
|
||||
const key = node.getAttribute("t-key");
|
||||
node.removeAttribute("t-key");
|
||||
const memo = node.getAttribute("t-memo") || "";
|
||||
node.removeAttribute("t-memo");
|
||||
const body = parseNode(node, ctx);
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasNoTCall = !html.includes("t-call");
|
||||
const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
|
||||
const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
|
||||
const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
|
||||
const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
|
||||
|
||||
return {
|
||||
type: ASTType.TForEach,
|
||||
collection,
|
||||
elem,
|
||||
body,
|
||||
memo,
|
||||
key,
|
||||
isOnlyChild: false,
|
||||
hasNoComponent: hasNoComponent(body),
|
||||
hasNoFirst,
|
||||
hasNoLast,
|
||||
hasNoIndex,
|
||||
hasNoValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if we are sure the ast does not contain any component
|
||||
*/
|
||||
function hasNoComponent(ast: AST): boolean {
|
||||
switch (ast.type) {
|
||||
case ASTType.TComponent:
|
||||
case ASTType.TRaw:
|
||||
case ASTType.TCall:
|
||||
case ASTType.TCallBlock:
|
||||
case ASTType.TSlot:
|
||||
return false;
|
||||
case ASTType.TSet:
|
||||
case ASTType.Text:
|
||||
case ASTType.Comment:
|
||||
case ASTType.TEsc:
|
||||
return true;
|
||||
case ASTType.TKey:
|
||||
return hasNoComponent(ast.content);
|
||||
case ASTType.TDebug:
|
||||
case ASTType.TLog:
|
||||
return ast.content ? hasNoComponent(ast.content) : true;
|
||||
case ASTType.TForEach:
|
||||
return ast.hasNoComponent;
|
||||
case ASTType.Multi:
|
||||
case ASTType.DomNode: {
|
||||
for (let elem of ast.content) {
|
||||
if (!hasNoComponent(elem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case ASTType.TIf: {
|
||||
if (!hasNoComponent(ast.content)) {
|
||||
return false;
|
||||
}
|
||||
if (ast.tElif) {
|
||||
for (let elem of ast.tElif) {
|
||||
if (!hasNoComponent(elem.content)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ast.tElse && !hasNoComponent(ast.tElse)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseTKey(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-key")) {
|
||||
return null;
|
||||
}
|
||||
const key = node.getAttribute("t-key")!;
|
||||
node.removeAttribute("t-key");
|
||||
const body = parseNode(node, ctx);
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
return { type: ASTType.TKey, expr: key, content: body };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-call
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-call")) {
|
||||
return null;
|
||||
}
|
||||
const subTemplate = node.getAttribute("t-call")!;
|
||||
|
||||
node.removeAttribute("t-call");
|
||||
if (node.tagName !== "t") {
|
||||
const ast = parseNode(node, ctx);
|
||||
const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null };
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
ast.content = [tcall];
|
||||
return ast;
|
||||
}
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: tcall },
|
||||
};
|
||||
}
|
||||
}
|
||||
const body: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const ast = parseNode(child, ctx);
|
||||
if (ast) {
|
||||
body.push(ast);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: ASTType.TCall,
|
||||
name: subTemplate,
|
||||
body: body.length ? body : null,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-call-block
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTCallBlock(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-call-block")) {
|
||||
return null;
|
||||
}
|
||||
const name = node.getAttribute("t-call-block")!;
|
||||
return {
|
||||
type: ASTType.TCallBlock,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-if
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTIf(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-if")) {
|
||||
return null;
|
||||
}
|
||||
const condition = node.getAttribute("t-if")!;
|
||||
node.removeAttribute("t-if");
|
||||
const content = parseNode(node, ctx);
|
||||
if (!content) {
|
||||
throw new Error("hmmm");
|
||||
}
|
||||
|
||||
let nextElement = node.nextElementSibling;
|
||||
// t-elifs
|
||||
const tElifs: any[] = [];
|
||||
while (nextElement && nextElement.hasAttribute("t-elif")) {
|
||||
const condition = nextElement.getAttribute("t-elif");
|
||||
nextElement.removeAttribute("t-elif");
|
||||
const tElif = parseNode(nextElement, ctx);
|
||||
const next = nextElement.nextElementSibling;
|
||||
nextElement.remove();
|
||||
nextElement = next;
|
||||
if (tElif) {
|
||||
tElifs.push({ condition, content: tElif });
|
||||
}
|
||||
}
|
||||
|
||||
// t-else
|
||||
let tElse: AST | null = null;
|
||||
if (nextElement && nextElement.hasAttribute("t-else")) {
|
||||
nextElement.removeAttribute("t-else");
|
||||
tElse = parseNode(nextElement, ctx);
|
||||
nextElement.remove();
|
||||
}
|
||||
|
||||
return {
|
||||
type: ASTType.TIf,
|
||||
condition,
|
||||
content,
|
||||
tElif: tElifs.length ? tElifs : null,
|
||||
tElse,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-set directive
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-set")) {
|
||||
return null;
|
||||
}
|
||||
const name = node.getAttribute("t-set")!;
|
||||
const value = node.getAttribute("t-value") || null;
|
||||
const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
|
||||
let body: AST[] | null = null;
|
||||
if (node.textContent !== node.innerHTML) {
|
||||
body = [];
|
||||
for (let child of node.childNodes) {
|
||||
let childAst = parseNode(child, ctx);
|
||||
if (childAst) {
|
||||
body.push(childAst);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: ASTType.TSet, name, value, defaultValue, body };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Components
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
let name = node.tagName;
|
||||
const firstLetter = name[0];
|
||||
let isDynamic = node.hasAttribute("t-component");
|
||||
|
||||
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
|
||||
return null;
|
||||
}
|
||||
if (isDynamic) {
|
||||
name = node.getAttribute("t-component")!;
|
||||
node.removeAttribute("t-component");
|
||||
}
|
||||
|
||||
const props: ASTComponent["props"] = {};
|
||||
const handlers: ASTComponent["handlers"] = {};
|
||||
for (let name of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(name)!;
|
||||
if (name.startsWith("t-on-")) {
|
||||
handlers[name.slice(5)] = value;
|
||||
} else {
|
||||
props[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const slots: ASTComponent["slots"] = {};
|
||||
if (node.hasChildNodes()) {
|
||||
const clone = <Element>node.cloneNode(true);
|
||||
|
||||
// named slots
|
||||
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
||||
for (let slotNode of slotNodes) {
|
||||
const name = slotNode.getAttribute("t-set-slot")!;
|
||||
|
||||
// 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!;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
slotNode.removeAttribute("t-set-slot");
|
||||
slotNode.remove();
|
||||
const slotAst = parseNode(slotNode, ctx);
|
||||
if (slotAst) {
|
||||
slots[name] = slotAst;
|
||||
}
|
||||
}
|
||||
|
||||
// default slot
|
||||
const defaultContent = parseChildNodes(clone, ctx);
|
||||
if (defaultContent) {
|
||||
slots.default = defaultContent;
|
||||
}
|
||||
}
|
||||
return { type: ASTType.TComponent, name, isDynamic, props, handlers, slots };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Slots
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-slot")) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: ASTType.TSlot,
|
||||
name: node.getAttribute("t-slot")!,
|
||||
defaultContent: parseChildNodes(node, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
|
||||
const children: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const childAst = parseNode(child, ctx);
|
||||
if (childAst) {
|
||||
children.push(childAst);
|
||||
}
|
||||
}
|
||||
switch (children.length) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return children[0];
|
||||
default:
|
||||
return { type: ASTType.Multi, content: children };
|
||||
}
|
||||
}
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
let tbranch = doc.querySelectorAll("[t-elif], [t-else]");
|
||||
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
||||
let node = tbranch[i];
|
||||
let prevElem = node.previousElementSibling!;
|
||||
let pattr = (name: string) => prevElem.getAttribute(name);
|
||||
let nattr = (name: string) => +!!node.getAttribute(name);
|
||||
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
||||
if (pattr("t-foreach")) {
|
||||
throw new Error(
|
||||
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
||||
);
|
||||
}
|
||||
if (
|
||||
["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
|
||||
return a + b;
|
||||
}) > 1
|
||||
) {
|
||||
throw new Error("Only one conditional branching directive is allowed per node");
|
||||
}
|
||||
// All text (with only spaces) and comment nodes (nodeType 8) between
|
||||
// branch nodes are removed
|
||||
let textNode;
|
||||
while ((textNode = node.previousSibling) !== prevElem) {
|
||||
if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) {
|
||||
throw new Error("text is not allowed between branching directives");
|
||||
}
|
||||
textNode!.remove();
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
@@ -1,921 +0,0 @@
|
||||
import { EventBus } from "../core/event_bus";
|
||||
import { h, patch, VNode } from "../vdom/index";
|
||||
import { CompilationContext } from "./compilation_context";
|
||||
import { shallowEqual } from "../utils";
|
||||
import { addNS } from "../vdom/vdom";
|
||||
|
||||
/**
|
||||
* Owl QWeb Engine
|
||||
*
|
||||
* In this file, you will find a QWeb engine/template compiler. It is the core
|
||||
* of how Owl component works.
|
||||
*
|
||||
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
|
||||
* DOM representation.
|
||||
*
|
||||
* We have here:
|
||||
* - a CompilationContext class, which is an internal object that contains all
|
||||
* compilation specific information, while a template is being compiled.
|
||||
* - a QWeb class: this is the code of the QWeb compiler.
|
||||
*
|
||||
* Note that this file does not contain the implementation of the QWeb
|
||||
* directives (see qweb_directives.ts and qweb_extensions.ts).
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
export type EvalContext = { [key: string]: any };
|
||||
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
|
||||
|
||||
interface Template {
|
||||
elem: Element;
|
||||
fn: CompiledTemplate;
|
||||
}
|
||||
|
||||
interface CompilationInfo {
|
||||
node: Element;
|
||||
qweb: QWeb;
|
||||
ctx: CompilationContext;
|
||||
fullName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface NodeCreationCompilationInfo extends CompilationInfo {
|
||||
nodeID: number;
|
||||
addNodeHook: Function;
|
||||
}
|
||||
|
||||
export interface Directive {
|
||||
name: string;
|
||||
extraNames?: string[];
|
||||
priority: number;
|
||||
// if return true, then directive is fully applied and there is no need to
|
||||
// keep processing node. Otherwise, we keep going.
|
||||
atNodeEncounter?(info: CompilationInfo): boolean | void;
|
||||
atNodeCreation?(info: NodeCreationCompilationInfo): void;
|
||||
finalize?(info: CompilationInfo): void;
|
||||
}
|
||||
|
||||
interface QWebConfig {
|
||||
templates?: string;
|
||||
translateFn?(text: string): string;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Const/global stuff/helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
|
||||
const lineBreakRE = /[\r\n]/;
|
||||
const whitespaceRE = /\s+/g;
|
||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||
|
||||
const NODE_HOOKS_PARAMS = {
|
||||
create: "(_, n)",
|
||||
insert: "vn",
|
||||
remove: "(vn, rm)",
|
||||
destroy: "()",
|
||||
};
|
||||
|
||||
interface Utils {
|
||||
toClassObj(expr: any): Object;
|
||||
shallowEqual(p1: Object, p2: Object): boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function isComponent(obj): boolean {
|
||||
return obj && obj.hasOwnProperty("__owl__");
|
||||
}
|
||||
|
||||
class VDomArray extends Array {
|
||||
toString() {
|
||||
return vDomToString(this);
|
||||
}
|
||||
}
|
||||
|
||||
function vDomToString(vdom: VNode[]): string {
|
||||
return vdom
|
||||
.map((vnode) => {
|
||||
if (vnode.sel) {
|
||||
const node = document.createElement(vnode.sel);
|
||||
const result = patch(node, vnode);
|
||||
return (<HTMLElement>result.elm).outerHTML;
|
||||
} else {
|
||||
return vnode.text;
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
const UTILS: Utils = {
|
||||
zero: Symbol("zero"),
|
||||
toClassObj(expr) {
|
||||
const result = {};
|
||||
if (typeof expr === "string") {
|
||||
// we transform here a list of classes into an object:
|
||||
// 'hey you' becomes {hey: true, you: true}
|
||||
expr = expr.trim();
|
||||
if (!expr) {
|
||||
return {};
|
||||
}
|
||||
let words = expr.split(/\s+/);
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
result[words[i]] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// this is already an object, but we may need to split keys:
|
||||
// {'a b': true, 'a c': false} should become {a: true, b: true, c: false}
|
||||
for (let key in expr) {
|
||||
const value = expr[key];
|
||||
const words = key.split(/\s+/);
|
||||
for (let word of words) {
|
||||
result[word] = result[word] || value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* This method combines the current context with the variables defined in a
|
||||
* scope for use in a slot.
|
||||
*
|
||||
* The implementation is kind of tricky because we want to preserve the
|
||||
* prototype chain structure of the cloned result. So we need to traverse the
|
||||
* prototype chain, cloning each level respectively.
|
||||
*/
|
||||
combine(context, scope) {
|
||||
let clone = context;
|
||||
const scopeStack = [];
|
||||
while (!isComponent(scope)) {
|
||||
scopeStack.push(scope);
|
||||
scope = scope.__proto__;
|
||||
}
|
||||
while (scopeStack.length) {
|
||||
let scope = scopeStack.pop();
|
||||
clone = Object.create(clone);
|
||||
Object.assign(clone, scope);
|
||||
}
|
||||
return clone;
|
||||
},
|
||||
shallowEqual,
|
||||
addNameSpace(vnode) {
|
||||
addNS(vnode.data, vnode.children, vnode.sel);
|
||||
},
|
||||
VDomArray,
|
||||
vDomToString,
|
||||
getComponent(obj) {
|
||||
while (obj && !isComponent(obj)) {
|
||||
obj = obj.__proto__;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
getScope(obj, property: string) {
|
||||
const obj0 = obj;
|
||||
while (
|
||||
obj &&
|
||||
!obj.hasOwnProperty(property) &&
|
||||
!(obj.hasOwnProperty("__access_mode__") && obj.__access_mode__ === "ro")
|
||||
) {
|
||||
const newObj = obj.__proto__;
|
||||
if (!newObj || isComponent(newObj)) {
|
||||
return obj0;
|
||||
}
|
||||
obj = newObj;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
};
|
||||
|
||||
function parseXML(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
|
||||
const doc = parser.parseFromString(xml, "text/xml");
|
||||
if (doc.getElementsByTagName("parsererror").length) {
|
||||
let msg = "Invalid XML in template.";
|
||||
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
||||
if (parsererrorText) {
|
||||
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
||||
const re = /\d+/g;
|
||||
const firstMatch = re.exec(parsererrorText);
|
||||
if (firstMatch) {
|
||||
const lineNumber = Number(firstMatch[0]);
|
||||
const line = xml.split("\n")[lineNumber - 1];
|
||||
const secondMatch = re.exec(parsererrorText);
|
||||
if (line && secondMatch) {
|
||||
const columnIndex = Number(secondMatch[0]) - 1;
|
||||
if (line[columnIndex]) {
|
||||
msg +=
|
||||
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
||||
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function escapeQuotes(str: string): string {
|
||||
return str.replace(/\'/g, "\\'");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// QWeb rendering engine
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class QWeb extends EventBus {
|
||||
templates: { [name: string]: Template };
|
||||
static utils = UTILS;
|
||||
static components = Object.create(null);
|
||||
|
||||
static DIRECTIVE_NAMES: { [key: string]: 1 } = {
|
||||
name: 1,
|
||||
att: 1,
|
||||
attf: 1,
|
||||
translation: 1,
|
||||
tag: 1,
|
||||
};
|
||||
static DIRECTIVES: Directive[] = [];
|
||||
|
||||
static TEMPLATES: { [name: string]: Template } = {};
|
||||
|
||||
static nextId: number = 1;
|
||||
|
||||
h = h;
|
||||
// dev mode enables better error messages or more costly validations
|
||||
static dev: boolean = false;
|
||||
static enableTransitions: boolean = true;
|
||||
|
||||
// slots contains sub templates defined with t-set inside t-component nodes, and
|
||||
// are meant to be used by the t-slot directive.
|
||||
static slots = {};
|
||||
static nextSlotId = 1;
|
||||
|
||||
// subTemplates are stored in two objects: a (local) mapping from a name to an
|
||||
// id, and a (global) mapping from an id to the compiled function. This is
|
||||
// necessary to ensure that global templates can be called with more than one
|
||||
// QWeb instance.
|
||||
subTemplates: { [key: string]: number } = {};
|
||||
static subTemplates: { [id: number]: Function } = {};
|
||||
|
||||
isUpdating: boolean = false;
|
||||
translateFn?: QWebConfig["translateFn"];
|
||||
|
||||
constructor(config: QWebConfig = {}) {
|
||||
super();
|
||||
this.templates = Object.create(QWeb.TEMPLATES);
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
}
|
||||
if (config.translateFn) {
|
||||
this.translateFn = config.translateFn;
|
||||
}
|
||||
}
|
||||
|
||||
static addDirective(directive: Directive) {
|
||||
if (directive.name in QWeb.DIRECTIVE_NAMES) {
|
||||
throw new Error(`Directive "${directive.name} already registered`);
|
||||
}
|
||||
QWeb.DIRECTIVES.push(directive);
|
||||
QWeb.DIRECTIVE_NAMES[directive.name] = 1;
|
||||
QWeb.DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
||||
if (directive.extraNames) {
|
||||
directive.extraNames.forEach((n) => (QWeb.DIRECTIVE_NAMES[n] = 1));
|
||||
}
|
||||
}
|
||||
|
||||
static registerComponent(name: string, Component: any) {
|
||||
if (QWeb.components[name]) {
|
||||
throw new Error(`Component '${name}' has already been registered`);
|
||||
}
|
||||
QWeb.components[name] = Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register globally a template. All QWeb instances will obtain their
|
||||
* templates from their own template map, and then, from the global static
|
||||
* TEMPLATES property.
|
||||
*/
|
||||
static registerTemplate(name: string, template: string) {
|
||||
if (QWeb.TEMPLATES[name]) {
|
||||
throw new Error(`Template '${name}' has already been registered`);
|
||||
}
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate(name, template);
|
||||
QWeb.TEMPLATES[name] = qweb.templates[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a template to the internal template map. Note that it is not
|
||||
* immediately compiled.
|
||||
*/
|
||||
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
|
||||
if (allowDuplicate && name in this.templates) {
|
||||
return;
|
||||
}
|
||||
const doc = parseXML(xmlString);
|
||||
if (!doc.firstChild) {
|
||||
throw new Error("Invalid template (should not be empty)");
|
||||
}
|
||||
this._addTemplate(name, <Element>doc.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load templates from a xml (as a string or xml document). This will look up
|
||||
* for the first <templates> tag, and will consider each child of this as a
|
||||
* template, with the name given by the t-name attribute.
|
||||
*/
|
||||
addTemplates(xmlstr: string | Document) {
|
||||
if (!xmlstr) {
|
||||
return;
|
||||
}
|
||||
const doc = typeof xmlstr === "string" ? parseXML(xmlstr) : xmlstr;
|
||||
const templates = doc.getElementsByTagName("templates")[0];
|
||||
if (!templates) {
|
||||
return;
|
||||
}
|
||||
for (let elem of <any>templates.children) {
|
||||
const name = elem.getAttribute("t-name");
|
||||
this._addTemplate(name, elem);
|
||||
}
|
||||
}
|
||||
|
||||
_addTemplate(name: string, elem: Element) {
|
||||
if (name in this.templates) {
|
||||
throw new Error(`Template ${name} already defined`);
|
||||
}
|
||||
this._processTemplate(elem);
|
||||
const template = {
|
||||
elem,
|
||||
fn: function (this: QWeb, context, extra) {
|
||||
const compiledFunction = this._compile(name);
|
||||
template.fn = compiledFunction;
|
||||
return compiledFunction.call(this, context, extra);
|
||||
},
|
||||
};
|
||||
this.templates[name] = template;
|
||||
}
|
||||
|
||||
_processTemplate(elem: Element) {
|
||||
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
|
||||
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
||||
let node = tbranch[i];
|
||||
let prevElem = node.previousElementSibling!;
|
||||
let pattr = function (name) {
|
||||
return prevElem.getAttribute(name);
|
||||
};
|
||||
let nattr = function (name) {
|
||||
return +!!node.getAttribute(name);
|
||||
};
|
||||
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
||||
if (pattr("t-foreach")) {
|
||||
throw new Error(
|
||||
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
||||
);
|
||||
}
|
||||
if (
|
||||
["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
|
||||
return a + b;
|
||||
}) > 1
|
||||
) {
|
||||
throw new Error("Only one conditional branching directive is allowed per node");
|
||||
}
|
||||
// All text (with only spaces) and comment nodes (nodeType 8) between
|
||||
// branch nodes are removed
|
||||
let textNode;
|
||||
while ((textNode = node.previousSibling) !== prevElem) {
|
||||
if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
|
||||
throw new Error("text is not allowed between branching directives");
|
||||
}
|
||||
textNode.remove();
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render a template
|
||||
*
|
||||
* @param {string} name the template should already have been added
|
||||
*/
|
||||
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
|
||||
const template = this.templates[name];
|
||||
if (!template) {
|
||||
throw new Error(`Template ${name} does not exist`);
|
||||
}
|
||||
return template.fn.call(this, context, extra);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template to a html string.
|
||||
*
|
||||
* Note that this is more limited than the `render` method: it is not suitable
|
||||
* to render a full component tree, since this is an asynchronous operation.
|
||||
* This method can only render templates without components.
|
||||
*/
|
||||
renderToString(name: string, context: EvalContext = {}, extra?: any): string {
|
||||
const vnode = this.render(name, context, extra);
|
||||
if (vnode.sel === undefined) {
|
||||
return vnode.text!;
|
||||
}
|
||||
const node = document.createElement(vnode.sel);
|
||||
const result = patch(node, vnode);
|
||||
return (result.elm as HTMLElement).outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force all widgets connected to this QWeb instance to rerender themselves.
|
||||
*
|
||||
* This method is mostly useful for external code that want to modify the
|
||||
* application in some cases. For example, a router plugin.
|
||||
*/
|
||||
forceUpdate() {
|
||||
this.isUpdating = true;
|
||||
Promise.resolve().then(() => {
|
||||
if (this.isUpdating) {
|
||||
this.isUpdating = false;
|
||||
this.trigger("update");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_compile(
|
||||
name: string,
|
||||
options: {
|
||||
elem?: Element;
|
||||
hasParent?: boolean;
|
||||
defineKey?: boolean;
|
||||
} = {}
|
||||
): CompiledTemplate {
|
||||
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 (options.hasParent) {
|
||||
ctx.variables = Object.create(null);
|
||||
ctx.parentNode = ctx.generateID();
|
||||
ctx.allowMultipleRoots = true;
|
||||
ctx.shouldDefineParent = true;
|
||||
ctx.hasParentWidget = true;
|
||||
ctx.shouldDefineResult = false;
|
||||
ctx.addLine(`let c${ctx.parentNode} = extra.parentNode;`);
|
||||
if (options.defineKey) {
|
||||
ctx.addLine(`let key0 = extra.key || "";`);
|
||||
ctx.hasKey0 = true;
|
||||
}
|
||||
}
|
||||
this._compileNode(elem, ctx);
|
||||
|
||||
if (!options.hasParent) {
|
||||
if (ctx.shouldDefineResult) {
|
||||
ctx.addLine(`return result;`);
|
||||
} else {
|
||||
if (!ctx.rootNode) {
|
||||
throw new Error(`A template should have one root node (${ctx.templateName})`);
|
||||
}
|
||||
ctx.addLine(`return vn${ctx.rootNode};`);
|
||||
}
|
||||
}
|
||||
|
||||
let code = ctx.generateCode();
|
||||
const templateName = ctx.templateName.replace(/`/g, "'").slice(0, 200);
|
||||
code.unshift(` // Template name: "${templateName}"`);
|
||||
|
||||
let template;
|
||||
try {
|
||||
template = new Function("context, extra", code.join("\n")) as CompiledTemplate;
|
||||
} catch (e) {
|
||||
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
||||
console.warn(code.join("\n"));
|
||||
console.groupEnd();
|
||||
throw new Error(
|
||||
`Invalid generated code while compiling template '${templateName}': ${e.message}`
|
||||
);
|
||||
}
|
||||
if (isDebug) {
|
||||
const tpl = this.templates[name];
|
||||
if (tpl) {
|
||||
const msg = `Template: ${tpl.elem.outerHTML}\nCompiled code:\n${template.toString()}`;
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code from an xml node
|
||||
*
|
||||
*/
|
||||
_compileNode(node: ChildNode, ctx: CompilationContext) {
|
||||
if (!(node instanceof Element)) {
|
||||
// this is a text node, there are no directive to apply
|
||||
let text = node.textContent!;
|
||||
if (!ctx.inPreTag) {
|
||||
if (lineBreakRE.test(text) && !text.trim()) {
|
||||
return;
|
||||
}
|
||||
text = text.replace(whitespaceRE, " ");
|
||||
}
|
||||
if (this.translateFn) {
|
||||
if ((node.parentNode as any).getAttribute("t-translation") !== "off") {
|
||||
const match = translationRE.exec(text);
|
||||
text = match[1] + this.translateFn(match[2]) + match[3];
|
||||
}
|
||||
}
|
||||
if (ctx.parentNode) {
|
||||
if (node.nodeType === 3) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
||||
} else if (node.nodeType === 8) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(h('!', \`${text}\`));`);
|
||||
}
|
||||
} else if (ctx.parentTextNode) {
|
||||
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
||||
} else {
|
||||
// this is an unusual situation: this text node is the result of the
|
||||
// template rendering.
|
||||
let nodeID = ctx.generateID();
|
||||
ctx.addLine(`let vn${nodeID} = {text: \`${text}\`};`);
|
||||
ctx.addLine(`result = vn${nodeID};`);
|
||||
ctx.rootContext.rootNode = nodeID;
|
||||
ctx.rootContext.parentTextNode = nodeID;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.tagName !== "t" && node.hasAttribute("t-call")) {
|
||||
const tCallNode = document.implementation.createDocument(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"t",
|
||||
null
|
||||
).documentElement;
|
||||
tCallNode.setAttribute("t-call", node.getAttribute("t-call")!);
|
||||
node.removeAttribute("t-call");
|
||||
node.prepend(tCallNode);
|
||||
}
|
||||
|
||||
const firstLetter = node.tagName[0];
|
||||
if (firstLetter === firstLetter.toUpperCase()) {
|
||||
// this is a component, we modify in place the xml document to change
|
||||
// <SomeComponent ... /> to <SomeComponent t-component="SomeComponent" ... />
|
||||
node.setAttribute("t-component", node.tagName);
|
||||
} else if (node.tagName !== "t" && node.hasAttribute("t-component")) {
|
||||
throw new Error(
|
||||
`Directive 't-component' can only be used on <t> nodes (used on a <${node.tagName}>)`
|
||||
);
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
|
||||
const validDirectives: {
|
||||
directive: Directive;
|
||||
value: string;
|
||||
fullName: string;
|
||||
}[] = [];
|
||||
|
||||
const finalizers: typeof validDirectives = [];
|
||||
|
||||
// maybe this is not optimal: we iterate on all attributes here, and again
|
||||
// just after for each directive.
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let attrName = attributes[i].name;
|
||||
if (attrName.startsWith("t-")) {
|
||||
let dName = attrName.slice(2).split(/-|\./)[0];
|
||||
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
|
||||
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
||||
}
|
||||
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
|
||||
const tNode = document.implementation.createDocument(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"t",
|
||||
null
|
||||
).documentElement;
|
||||
tNode.setAttribute(attrName, node.getAttribute(attrName)!);
|
||||
for (let child of Array.from(node.childNodes)) {
|
||||
tNode.appendChild(child);
|
||||
}
|
||||
node.appendChild(tNode);
|
||||
node.removeAttribute(attrName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DIR_N = QWeb.DIRECTIVES.length;
|
||||
const ATTR_N = attributes.length;
|
||||
let withHandlers = false;
|
||||
for (let i = 0; i < DIR_N; i++) {
|
||||
let directive = QWeb.DIRECTIVES[i];
|
||||
let fullName;
|
||||
let value;
|
||||
for (let j = 0; j < ATTR_N; j++) {
|
||||
const name = attributes[j].name;
|
||||
if (
|
||||
name === "t-" + directive.name ||
|
||||
name.startsWith("t-" + directive.name + "-") ||
|
||||
name.startsWith("t-" + directive.name + ".")
|
||||
) {
|
||||
fullName = name;
|
||||
value = attributes[j].textContent;
|
||||
validDirectives.push({ directive, value, fullName });
|
||||
if (directive.name === "on" || directive.name === "model") {
|
||||
withHandlers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.finalize) {
|
||||
finalizers.push({ directive, value, fullName });
|
||||
}
|
||||
if (directive.atNodeEncounter) {
|
||||
const isDone = directive.atNodeEncounter({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
});
|
||||
if (isDone) {
|
||||
for (let { directive, value, fullName } of finalizers) {
|
||||
directive.finalize!({ node, qweb: this, ctx, fullName, value });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.nodeName !== "t" || node.hasAttribute("t-tag")) {
|
||||
let nodeHooks = {};
|
||||
let addNodeHook = function (hook, handler) {
|
||||
nodeHooks[hook] = nodeHooks[hook] || [];
|
||||
nodeHooks[hook].push(handler);
|
||||
};
|
||||
if (node.tagName === "select" && node.hasAttribute("t-att-value")) {
|
||||
const value = node.getAttribute("t-att-value");
|
||||
let exprId = ctx.generateID();
|
||||
ctx.addLine(`let expr${exprId} = ${ctx.formatExpression(value)};`);
|
||||
let expr = `expr${exprId}`;
|
||||
node.setAttribute("t-att-value", expr);
|
||||
addNodeHook("create", `n.elm.value=${expr};`);
|
||||
}
|
||||
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
||||
ctx = ctx.withParent(nodeID);
|
||||
|
||||
for (let { directive, value, fullName } of validDirectives) {
|
||||
if (directive.atNodeCreation) {
|
||||
directive.atNodeCreation({
|
||||
node,
|
||||
qweb: this,
|
||||
ctx,
|
||||
fullName,
|
||||
value,
|
||||
nodeID,
|
||||
addNodeHook,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(nodeHooks).length) {
|
||||
ctx.addLine(`p${nodeID}.hook = {`);
|
||||
for (let hook in nodeHooks) {
|
||||
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
|
||||
for (let handler of nodeHooks[hook]) {
|
||||
ctx.addLine(` ${handler}`);
|
||||
}
|
||||
ctx.addLine(` },`);
|
||||
}
|
||||
ctx.addLine(`};`);
|
||||
}
|
||||
}
|
||||
if (node.nodeName === "pre") {
|
||||
ctx = ctx.subContext("inPreTag", true);
|
||||
}
|
||||
|
||||
this._compileChildren(node, ctx);
|
||||
// svg support
|
||||
// we hadd svg namespace if it is a svg or if it is a g, but only if it is
|
||||
// the root node. This is the easiest way to support svg sub components:
|
||||
// they need to have a g tag as root. Otherwise, we would need a complete
|
||||
// list of allowed svg tags.
|
||||
const shouldAddNS =
|
||||
node.nodeName === "svg" || (node.nodeName === "g" && ctx.rootNode === ctx.parentNode);
|
||||
if (shouldAddNS) {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
ctx.addLine(`utils.addNameSpace(vn${ctx.parentNode});`);
|
||||
}
|
||||
|
||||
for (let { directive, value, fullName } of finalizers) {
|
||||
directive.finalize!({ node, qweb: this, ctx, fullName, value });
|
||||
}
|
||||
}
|
||||
|
||||
_compileGenericNode(
|
||||
node: ChildNode,
|
||||
ctx: CompilationContext,
|
||||
withHandlers: boolean = true
|
||||
): number {
|
||||
// nodeType 1 is generic tag
|
||||
if (node.nodeType !== 1) {
|
||||
throw new Error("unsupported node type");
|
||||
}
|
||||
const attributes = (<Element>node).attributes;
|
||||
const attrs: string[] = [];
|
||||
const props: string[] = [];
|
||||
const tattrs: number[] = [];
|
||||
|
||||
function handleProperties(key, val) {
|
||||
let isProp = false;
|
||||
switch (node.nodeName) {
|
||||
case "input":
|
||||
let type = (<Element>node).getAttribute("type");
|
||||
if (type === "checkbox" || type === "radio") {
|
||||
if (key === "checked" || key === "indeterminate") {
|
||||
isProp = true;
|
||||
}
|
||||
}
|
||||
if (key === "value" || key === "readonly" || key === "disabled") {
|
||||
isProp = true;
|
||||
}
|
||||
break;
|
||||
case "option":
|
||||
isProp = key === "selected" || key === "disabled";
|
||||
break;
|
||||
case "textarea":
|
||||
isProp = key === "readonly" || key === "disabled" || key === "value";
|
||||
break;
|
||||
case "select":
|
||||
isProp = key === "disabled" || key === "value";
|
||||
break;
|
||||
case "button":
|
||||
case "optgroup":
|
||||
isProp = key === "disabled";
|
||||
break;
|
||||
}
|
||||
if (isProp) {
|
||||
props.push(`${key}: ${val}`);
|
||||
}
|
||||
}
|
||||
let classObj = "";
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
let name = attributes[i].name;
|
||||
let value = attributes[i].textContent!;
|
||||
|
||||
if (this.translateFn && TRANSLATABLE_ATTRS.includes(name)) {
|
||||
value = this.translateFn(value);
|
||||
}
|
||||
|
||||
// regular attributes
|
||||
if (!name.startsWith("t-") && !(<Element>node).getAttribute("t-attf-" + name)) {
|
||||
const attID = ctx.generateID();
|
||||
if (name === "class") {
|
||||
if ((value = value.trim())) {
|
||||
let classDef = value
|
||||
.split(/\s+/)
|
||||
.map((a) => `'${escapeQuotes(a)}':true`)
|
||||
.join(",");
|
||||
if (classObj) {
|
||||
ctx.addLine(`Object.assign(${classObj}, {${classDef}})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = {${classDef}};`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.addLine(`let _${attID} = '${escapeQuotes(value)}';`);
|
||||
if (!name.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
name = '"' + name + '"';
|
||||
}
|
||||
attrs.push(`${name}: _${attID}`);
|
||||
handleProperties(name, `_${attID}`);
|
||||
}
|
||||
}
|
||||
|
||||
// dynamic attributes
|
||||
if (name.startsWith("t-att-")) {
|
||||
let attName = name.slice(6);
|
||||
const v = ctx.getValue(value);
|
||||
let formattedValue = typeof v === "string" ? ctx.formatExpression(v) : `scope.${v.id}`;
|
||||
|
||||
if (attName === "class") {
|
||||
ctx.rootContext.shouldDefineUtils = true;
|
||||
formattedValue = `utils.toClassObj(${formattedValue})`;
|
||||
if (classObj) {
|
||||
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
|
||||
} else {
|
||||
classObj = `_${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${classObj} = ${formattedValue};`);
|
||||
}
|
||||
} else {
|
||||
const attID = ctx.generateID();
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
// we need to combine dynamic with non dynamic attributes:
|
||||
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
|
||||
const attValue = (<Element>node).getAttribute(attName);
|
||||
if (attValue) {
|
||||
const attValueID = ctx.generateID();
|
||||
ctx.addLine(`let _${attValueID} = ${formattedValue};`);
|
||||
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
||||
const attrIndex = attrs.findIndex((att) => att.startsWith(attName + ":"));
|
||||
attrs.splice(attrIndex, 1);
|
||||
}
|
||||
if (node.nodeName === "select" && attName === "value") {
|
||||
attrs.push(`${attName}: ${v}`);
|
||||
handleProperties(attName, v);
|
||||
} else {
|
||||
ctx.addLine(`let _${attID} = ${formattedValue};`);
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
handleProperties(attName, "_" + attID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (name.startsWith("t-attf-")) {
|
||||
let attName = name.slice(7);
|
||||
if (!attName.match(/^[a-zA-Z]+$/)) {
|
||||
// attribute contains 'non letters' => we want to quote it
|
||||
attName = '"' + attName + '"';
|
||||
}
|
||||
const formattedExpr = ctx.interpolate(value);
|
||||
const attID = ctx.generateID();
|
||||
let staticVal = (<Element>node).getAttribute(attName);
|
||||
if (staticVal) {
|
||||
ctx.addLine(`let _${attID} = '${staticVal} ' + ${formattedExpr};`);
|
||||
} else {
|
||||
ctx.addLine(`let _${attID} = ${formattedExpr};`);
|
||||
}
|
||||
attrs.push(`${attName}: _${attID}`);
|
||||
}
|
||||
|
||||
// t-att= attributes
|
||||
if (name === "t-att") {
|
||||
let id = ctx.generateID();
|
||||
ctx.addLine(`let _${id} = ${ctx.formatExpression(value!)};`);
|
||||
tattrs.push(id);
|
||||
}
|
||||
}
|
||||
let nodeID = ctx.generateID();
|
||||
let key = ctx.loopNumber || ctx.hasKey0 ? `\`\${key${ctx.loopNumber}}_${nodeID}\`` : nodeID;
|
||||
const parts = [`key:${key}`];
|
||||
if (attrs.length + tattrs.length > 0) {
|
||||
parts.push(`attrs:{${attrs.join(",")}}`);
|
||||
}
|
||||
if (props.length > 0) {
|
||||
parts.push(`props:{${props.join(",")}}`);
|
||||
}
|
||||
if (classObj) {
|
||||
parts.push(`class:${classObj}`);
|
||||
}
|
||||
if (withHandlers) {
|
||||
parts.push(`on:{}`);
|
||||
}
|
||||
|
||||
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
||||
for (let id of tattrs) {
|
||||
ctx.addIf(`_${id} instanceof Array`);
|
||||
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
|
||||
ctx.addElse();
|
||||
ctx.addLine(`for (let key in _${id}) {`);
|
||||
ctx.indent();
|
||||
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
|
||||
ctx.dedent();
|
||||
ctx.addLine(`}`);
|
||||
ctx.closeIf();
|
||||
}
|
||||
let nodeName = `'${node.nodeName}'`;
|
||||
if ((<Element>node).hasAttribute("t-tag")) {
|
||||
const tagExpr = (<Element>node).getAttribute("t-tag");
|
||||
(<Element>node).removeAttribute("t-tag");
|
||||
nodeName = `tag${ctx.generateID()}`;
|
||||
ctx.addLine(`let ${nodeName} = ${ctx.formatExpression(tagExpr)};`);
|
||||
}
|
||||
ctx.addLine(`let vn${nodeID} = h(${nodeName}, p${nodeID}, c${nodeID});`);
|
||||
if (ctx.parentNode) {
|
||||
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
||||
} else if (ctx.loopNumber || ctx.hasKey0) {
|
||||
ctx.rootContext.shouldDefineResult = true;
|
||||
ctx.addLine(`result = vn${nodeID};`);
|
||||
}
|
||||
|
||||
return nodeID;
|
||||
}
|
||||
|
||||
_compileChildren(node: ChildNode, ctx: CompilationContext) {
|
||||
if (node.childNodes.length > 0) {
|
||||
for (let child of Array.from(node.childNodes)) {
|
||||
this._compileNode(child, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// import { compileTemplate, Template } from "./qweb/index";
|
||||
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
|
||||
import { component } from "../component/component_node";
|
||||
import { Template, compileTemplate } from "./compiler";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component };
|
||||
|
||||
export const globalTemplates: { [key: string]: string } = {};
|
||||
|
||||
function withDefault(value: any, defaultValue: any): any {
|
||||
return value === undefined || value === null || value === false ? defaultValue : value;
|
||||
}
|
||||
|
||||
function callSlot(
|
||||
ctx: any,
|
||||
parent: any,
|
||||
key: string,
|
||||
name: string,
|
||||
defaultSlot?: (ctx: any, key: string) => BDom,
|
||||
dynamic?: boolean
|
||||
): BDom | null {
|
||||
const slots = ctx.__owl__.slots;
|
||||
const slotFn = slots[name];
|
||||
const slotBDom = slotFn ? slotFn(parent, key) : null;
|
||||
if (defaultSlot) {
|
||||
let child1: BDom | undefined = undefined;
|
||||
let child2: BDom | undefined = undefined;
|
||||
// const result = new BMulti(2);
|
||||
if (slotBDom) {
|
||||
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
||||
} else {
|
||||
child2 = defaultSlot(parent, key);
|
||||
}
|
||||
return multi([child1, child2]);
|
||||
}
|
||||
return slotBDom;
|
||||
}
|
||||
|
||||
function capture(ctx: any): any {
|
||||
const component = ctx.__owl__.component;
|
||||
const result = Object.create(component);
|
||||
for (let k in ctx) {
|
||||
result[k] = ctx[k];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withKey(elem: any, k: string) {
|
||||
elem.key = k;
|
||||
return elem;
|
||||
}
|
||||
|
||||
function prepareList(collection: any): [any[], any[], number, any[]] {
|
||||
let keys: any[];
|
||||
let values: any[];
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
keys = collection;
|
||||
values = collection;
|
||||
} else if (collection) {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
} else {
|
||||
throw new Error("Invalid loop expression");
|
||||
}
|
||||
const n = values.length;
|
||||
return [keys, values, n, new Array(n)];
|
||||
}
|
||||
export const UTILS = {
|
||||
// elem,
|
||||
// setText,
|
||||
withDefault,
|
||||
zero: Symbol("zero"),
|
||||
callSlot,
|
||||
capture,
|
||||
// toClassObj,
|
||||
withKey,
|
||||
prepareList,
|
||||
shallowEqual,
|
||||
};
|
||||
|
||||
export class TemplateSet {
|
||||
rawTemplates: { [name: string]: string } = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
utils: typeof UTILS;
|
||||
|
||||
constructor() {
|
||||
const call = (subTemplate: string, ctx: any, parent: any) => {
|
||||
const template = this.getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template(ctx, parent));
|
||||
};
|
||||
|
||||
const getTemplate = (name: string) => this.getTemplate(name);
|
||||
this.utils = Object.assign({}, UTILS, { getTemplate, call });
|
||||
}
|
||||
|
||||
addTemplate(name: string, template: string, options: { allowDuplicate?: boolean } = {}) {
|
||||
if (name in this.rawTemplates && !options.allowDuplicate) {
|
||||
throw new Error(`Template ${name} already defined`);
|
||||
}
|
||||
this.rawTemplates[name] = template;
|
||||
}
|
||||
|
||||
getTemplate(name: string): Template {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
throw new Error(`Missing template: "${name}"`);
|
||||
}
|
||||
const templateFn = compileTemplate(rawTemplate, name);
|
||||
|
||||
// first add a function to lazily get the template, in case there is a
|
||||
// recursive call to the template name
|
||||
this.templates[name] = (context, parent) => this.templates[name](context, parent);
|
||||
const template = templateFn(bdom, this.utils);
|
||||
this.templates[name] = template;
|
||||
}
|
||||
return this.templates[name];
|
||||
}
|
||||
}
|
||||
|
||||
function shallowEqual(l1: any[], l2: any[]): boolean {
|
||||
for (let i = 0, l = l1.length; i < l; i++) {
|
||||
if (l1[i] !== l2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user