mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
8c17bb0411
Closes #473 Closes #484 Closes #489 Closes #492 Closes #512 Co-authored-by: Aaron Bohy <aab@odoo.com>
229 lines
6.9 KiB
TypeScript
229 lines
6.9 KiB
TypeScript
import { compileExpr, 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 = null;
|
|
parentTextNode: number | null = null;
|
|
rootNode: number | null = null;
|
|
indentLevel: number = 0;
|
|
rootContext: CompilationContext;
|
|
caller: Element | undefined;
|
|
shouldDefineOwner: boolean = false;
|
|
shouldDefineParent: boolean = false;
|
|
shouldDefineQWeb: boolean = false;
|
|
shouldDefineUtils: boolean = false;
|
|
shouldDefineRefs: boolean = false;
|
|
shouldDefineResult: boolean = true;
|
|
shouldProtectContext: boolean = false;
|
|
shouldTrackScope: boolean = false;
|
|
loopNumber: number = 0;
|
|
inPreTag: boolean = false;
|
|
templateName: string;
|
|
allowMultipleRoots: boolean = false;
|
|
hasParentWidget: boolean = false;
|
|
scopeVars: any[] = [];
|
|
currentKey: string = "";
|
|
templates: { [key: string]: boolean } = {};
|
|
callingLevel: number = 0;
|
|
inliningLevel: number = 0;
|
|
|
|
constructor(name?: string) {
|
|
this.rootContext = this;
|
|
this.templateName = name || "noname";
|
|
this.templates[this.templateName] = true;
|
|
this.addLine("var 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(): string {
|
|
const id = this.generateID();
|
|
let locationExpr = `\`__${this.generateID()}__`;
|
|
for (let i = 0; i < this.loopNumber - 1; i++) {
|
|
locationExpr += `\${i${i + 1}}__`;
|
|
}
|
|
if (this.currentKey) {
|
|
const k = this.currentKey;
|
|
this.addLine(`let k${id} = ${locationExpr}\` + ${k};`);
|
|
} else {
|
|
locationExpr += this.loopNumber ? `\${i${this.loopNumber}}__\`` : "`";
|
|
this.addLine(`let k${id} = ${locationExpr};`);
|
|
}
|
|
return `k${id}`;
|
|
}
|
|
|
|
generateCode(): string[] {
|
|
const shouldTrackScope = this.shouldTrackScope && this.scopeVars.length;
|
|
if (shouldTrackScope) {
|
|
// add some vars to scope if needed
|
|
for (let scopeVar of this.scopeVars.reverse()) {
|
|
let { index, key, indent } = scopeVar;
|
|
const prefix = new Array(indent + 2).join(" ");
|
|
this.code.splice(index + 1, 0, prefix + `scope.${key} = context.${key};`);
|
|
}
|
|
this.code.unshift(" const scope = Object.create(null);");
|
|
}
|
|
if (this.shouldProtectContext) {
|
|
this.code.unshift(" context = Object.create(context);");
|
|
}
|
|
if (this.shouldDefineResult) {
|
|
this.code.unshift(" let result;");
|
|
}
|
|
if (this.shouldDefineRefs) {
|
|
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
|
|
}
|
|
if (this.shouldDefineOwner) {
|
|
// this is necessary to prevent some directives (t-forach for ex) to
|
|
// pollute the rendering context by adding some keys in it.
|
|
this.code.unshift(" let owner = context;");
|
|
}
|
|
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;
|
|
if (key === "caller") {
|
|
newContext.callingLevel++;
|
|
newContext.inliningLevel++;
|
|
}
|
|
return newContext;
|
|
}
|
|
|
|
indent() {
|
|
this.indentLevel++;
|
|
}
|
|
|
|
dedent() {
|
|
this.indentLevel--;
|
|
}
|
|
|
|
addLine(line: string): number {
|
|
const prefix = new Array(this.indentLevel + 2).join(" ");
|
|
this.code.push(prefix + line);
|
|
return this.code.length - 1;
|
|
}
|
|
|
|
addToScope(key: string, expr: string) {
|
|
const index = this.addLine(`context.${key} = ${expr};`);
|
|
this.rootContext.scopeVars.push({ index, key, indent: this.indentLevel });
|
|
}
|
|
|
|
addIf(condition: string) {
|
|
this.addLine(`if (${condition}) {`);
|
|
this.indent();
|
|
}
|
|
|
|
addElse() {
|
|
this.dedent();
|
|
this.addLine("} else {");
|
|
this.indent();
|
|
}
|
|
|
|
closeIf() {
|
|
this.dedent();
|
|
this.addLine("}");
|
|
}
|
|
/**
|
|
* Recursively (inverse) fetches the `caller` of a context
|
|
* Useful to determine to which t-call a t-raw="0" refers
|
|
*/
|
|
getCaller(targetLevel?: number): Element | null {
|
|
if (targetLevel === undefined) {
|
|
targetLevel = this.inliningLevel;
|
|
}
|
|
if (targetLevel === this.callingLevel) {
|
|
return this.caller || null;
|
|
}
|
|
const proto = (this as any).__proto__;
|
|
return proto ? proto.getCaller(targetLevel) : null;
|
|
}
|
|
/**
|
|
* Marks the context with the current recursive level
|
|
* in which we are for inlining archs (t-raw="0")
|
|
*/
|
|
getInliningContext(): CompilationContext {
|
|
return this.subContext("inliningLevel", this.inliningLevel - 1);
|
|
}
|
|
|
|
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 {
|
|
return compileExpr(expr, this.variables);
|
|
}
|
|
|
|
/**
|
|
* 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 + "`";
|
|
}
|
|
}
|