Files
owl/src/qweb/compilation_context.ts
T

211 lines
6.2 KiB
TypeScript
Raw Normal View History

2020-01-09 10:02:27 +01:00
import { compileExpr, compileExprToArray, QWebVar } from "./expression_parser";
2019-07-14 22:18:00 +02:00
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
2019-07-14 22:18:00 +02:00
//------------------------------------------------------------------------------
// Compilation Context
//------------------------------------------------------------------------------
export class CompilationContext {
2019-11-27 15:07:10 +01:00
static nextID: number = 1;
2019-07-14 22:18:00 +02:00
code: string[] = [];
variables: { [key: string]: QWebVar } = {};
escaping: boolean = false;
parentNode: number | null | string = null;
2019-07-14 22:18:00 +02:00
parentTextNode: number | null = null;
rootNode: number | null = null;
indentLevel: number = 0;
rootContext: CompilationContext;
2019-07-14 22:18:00 +02:00
shouldDefineParent: boolean = false;
shouldDefineScope: boolean = false;
protectedScopeNumber: number = 0;
2019-07-14 22:18:00 +02:00
shouldDefineQWeb: boolean = false;
shouldDefineUtils: boolean = false;
2019-09-26 21:56:01 +02:00
shouldDefineRefs: boolean = false;
2019-09-11 11:28:41 +02:00
shouldDefineResult: boolean = true;
loopNumber: number = 0;
2019-07-14 22:18:00 +02:00
inPreTag: boolean = false;
templateName: string;
allowMultipleRoots: boolean = false;
hasParentWidget: boolean = false;
hasKey0: boolean = false;
keyStack: boolean[] = [];
2019-07-14 22:18:00 +02:00
constructor(name?: string) {
this.rootContext = this;
this.templateName = name || "noname";
2019-12-12 22:27:52 +01:00
this.addLine("let h = this.h;");
2019-07-14 22:18:00 +02:00
}
generateID(): number {
2019-11-27 15:07:10 +01:00
return CompilationContext.nextID++;
2019-07-14 22:18:00 +02:00
}
/**
* 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)
*/
2019-12-12 12:18:08 +01:00
generateTemplateKey(prefix: string = ""): string {
const id = this.generateID();
if (this.loopNumber === 0 && !this.hasKey0) {
2019-12-12 12:18:08 +01:00
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}`;
}
2019-07-14 22:18:00 +02:00
generateCode(): string[] {
if (this.shouldDefineResult) {
this.code.unshift(" let result;");
}
if (this.shouldDefineScope) {
this.code.unshift(" let scope = Object.create(context);");
}
2019-09-26 21:56:01 +02:00
if (this.shouldDefineRefs) {
this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};");
}
2019-07-14 22:18:00 +02:00
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 {
2019-07-14 22:18:00 +02:00
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;
}
2019-10-11 12:44:21 +02:00
if (!this.parentNode && this.rootContext.shouldDefineResult) {
2019-09-11 14:01:49 +02:00
this.addLine(`result = vn${node};`);
2019-09-11 11:28:41 +02:00
}
2019-07-14 22:18:00 +02:00
return this.subContext("parentNode", node);
}
subContext(key: keyof CompilationContext, value: any): CompilationContext {
2019-07-14 22:18:00 +02:00
const newContext = Object.create(this);
newContext[key] = value;
return newContext;
}
indent() {
this.rootContext.indentLevel++;
2019-07-14 22:18:00 +02:00
}
dedent() {
this.rootContext.indentLevel--;
2019-07-14 22:18:00 +02:00
}
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 {
2019-07-14 22:18:00 +02:00
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;
2019-07-14 22:18:00 +02:00
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();
2020-01-09 10:02:27 +01:00
return tokens
.map(tok => {
if (tok.varName) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
}
tok.value = `${tok.varName}_${argId}`;
}
2020-01-09 10:02:27 +01:00
return tok.value;
})
.join("");
}
2019-07-14 22:18:00 +02:00
/**
* 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);
2019-07-14 22:18:00 +02:00
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;
2020-01-09 10:02:27 +01:00
const scopeExpr = codeBlock
? `Object.create(scope);`
: `Object.assign(Object.create(context), scope);`;
2019-12-12 22:27:52 +01:00
this.addLine(`let _origScope${protectID} = scope;`);
this.addLine(`scope = ${scopeExpr}`);
return protectID;
}
stopProtectScope(protectID: number) {
this.rootContext.protectedScopeNumber--;
this.addLine(`scope = _origScope${protectID};`);
}
}