[REF] move app and compiler code around

This commit is contained in:
Géry Debongnie
2021-11-13 09:02:34 +01:00
committed by Aaron Bohy
parent 9b8c582b32
commit 8c16790471
12 changed files with 116 additions and 113 deletions
+72 -96
View File
@@ -1,4 +1,3 @@
import { BDom } from "../blockdom";
import { compileExpr, compileExprToArray, interpolate, INTERP_REGEXP } from "./inline_expressions";
import {
AST,
@@ -20,24 +19,18 @@ import {
ASTTSet,
ASTTranslation,
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 interface CompileOptions {
name?: string;
export interface Config {
translateFn?: (s: string) => string;
translatableAttributes?: string[];
dev?: boolean;
}
export function compileTemplate(template: string, options?: CompileOptions): TemplateFunction {
const compiler = new QWebCompiler(template, options);
return compiler.compile();
export interface CodeGenOptions extends Config {
hasSafeContext?: boolean;
}
// using a non-html document so that <inner/outer>HTML serializes as XML instead
@@ -165,47 +158,37 @@ class CodeTarget {
}
}
export const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
export class QWebCompiler {
export class CodeGenerator {
blocks: BlockDescription[] = [];
nextId = 1;
nextBlockId = 1;
shouldProtectScope: boolean = false;
shouldDefineAssign: boolean = false;
hasSafeContext: boolean | null = null;
hasSafeContext: boolean;
hasRef: boolean = false;
// hasTCall: boolean = false;
isDebug: boolean = false;
functions: CodeTarget[] = [];
target = new CodeTarget("main");
templateName: string;
template: string;
dev: boolean;
translateFn: (s: string) => string;
translatableAttributes: string[];
ast: AST;
staticCalls: { id: string; template: string }[] = [];
constructor(template: string, options: CompileOptions = {}) {
this.template = template;
constructor(name: string, ast: AST, options: CodeGenOptions) {
this.translateFn = options.translateFn || ((s: string) => s);
this.translatableAttributes = options.translatableAttributes || TRANSLATABLE_ATTRS;
this.hasSafeContext = options.hasSafeContext || false;
this.dev = options.dev || false;
this.ast = parse(template);
if (options.name) {
this.templateName = options.name;
} else {
if (template.length > 250) {
this.templateName = template.slice(0, 250) + "...";
} else {
this.templateName = template;
}
}
this.ast = ast;
this.templateName = name;
}
compile(): TemplateFunction {
generateCode(): string {
const ast = this.ast;
this.isDebug = ast.type === ASTType.TDebug;
BlockDescription.nextBlockId = 1;
@@ -218,72 +201,7 @@ export class QWebCompiler {
translate: true,
tKeyExpr: null,
});
const code = this.generateCode();
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 = xmlDoc.createElement(tag);
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_block${parentBlock.id}`;
}
}
return block;
}
insertBlock(expression: string, block: BlockDescription, ctx: Context): void {
let blockExpr = block.generateExpr(expression);
const tKeyExpr = ctx.tKeyExpr;
if (block.parentVar) {
let keyArg = `key${this.target.loopLevel}`;
if (tKeyExpr) {
keyArg = `${tKeyExpr} + ${keyArg}`;
}
this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`);
return;
}
if (tKeyExpr) {
blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
}
if (block.isRoot && !ctx.preventRoot) {
this.addLine(`return ${blockExpr};`);
} else {
this.addLine(`let ${block.varName} = ${blockExpr};`);
}
}
generateCode(): string {
let mainCode = this.target.code;
this.target.code = [];
this.target.indentLevel = 0;
@@ -353,6 +271,67 @@ export class QWebCompiler {
return code;
}
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 = xmlDoc.createElement(tag);
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_block${parentBlock.id}`;
}
}
return block;
}
insertBlock(expression: string, block: BlockDescription, ctx: Context): void {
let blockExpr = block.generateExpr(expression);
const tKeyExpr = ctx.tKeyExpr;
if (block.parentVar) {
let keyArg = `key${this.target.loopLevel}`;
if (tKeyExpr) {
keyArg = `${tKeyExpr} + ${keyArg}`;
}
this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`);
return;
}
if (tKeyExpr) {
blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
}
if (block.isRoot && !ctx.preventRoot) {
this.addLine(`return ${blockExpr};`);
} else {
this.addLine(`let ${block.varName} = ${blockExpr};`);
}
}
generateFunctions(fn: CodeTarget) {
this.addLine("");
this.addLine(`const ${fn.name} = ${fn.signature}`);
@@ -992,9 +971,6 @@ export class QWebCompiler {
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");
+26
View File
@@ -0,0 +1,26 @@
import type { BDom } from "../blockdom";
import { CodeGenerator, Config } from "./code_generator";
import { parse } from "./parser";
export type Template = (context: any, vnode: any, key?: string) => BDom;
export type TemplateFunction = (blocks: any, utils: any) => Template;
interface CompileOptions extends Config {
name?: string;
}
export function compile(template: string, options: CompileOptions = {}): TemplateFunction {
// parsing
const ast = parse(template);
// some work
const hasSafeContext = !template.includes("t-set") && !template.includes("t-call");
const name = options.name || (template.length > 250 ? template.slice(0, 250) + "..." : template);
// code generation
const codeGenerator = new CodeGenerator(name, ast, { ...options, hasSafeContext });
const code = codeGenerator.generateCode();
// template function
return new Function("bdom, helpers", code) as TemplateFunction;
}
-162
View File
@@ -1,162 +0,0 @@
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
import { compileTemplate, Template } from "./code_generator";
import { component } from "../component/component_node";
import { validateProps } from "../component/props_validation";
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 {
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 || text("");
}
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)];
}
const isBoundary = Symbol("isBoundary");
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
const ctx0 = ctx;
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
const newCtx = ctx.__proto__;
if (!newCtx) {
ctx = ctx0;
break;
}
ctx = newCtx;
}
ctx[key] = value;
}
export const UTILS = {
// elem,
// setText,
withDefault,
zero: Symbol("zero"),
isBoundary,
callSlot,
capture,
// toClassObj,
withKey,
prepareList,
setContextValue,
shallowEqual,
toNumber,
validateProps,
};
export class TemplateSet {
rawTemplates: { [name: string]: string } = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
utils: typeof UTILS;
dev?: boolean;
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,
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
});
// 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 toNumber(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
}
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;
}