mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
initial commit
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Core Utility for Odoo Web Client
|
||||||
|
|
||||||
|
This is a POC, not at all production ready code!!!
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
roots: ["<rootDir>/src", "<rootDir>/tests"],
|
||||||
|
transform: {
|
||||||
|
"^.+\\.ts?$": "ts-jest"
|
||||||
|
},
|
||||||
|
testRegex: "(/tests/.*|(\\.|/)(test|spec))\\.(ts|js)x?$",
|
||||||
|
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"]
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "web-core",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Core Utils for Odoo Web Client",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -m amd --lib es2017,dom --target esnext --outfile dist/bundle.js",
|
||||||
|
"build:watch": "tsc --watch -m amd --lib es2017,dom --target esnext --outfile dist/bundle.js",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/ged-odoo/web-core.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/ged-odoo/web-core/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/ged-odoo/web-core#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^23.3.12",
|
||||||
|
"jest": "^23.6.0",
|
||||||
|
"ts-jest": "^23.10.5",
|
||||||
|
"typescript": "^3.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
+418
@@ -0,0 +1,418 @@
|
|||||||
|
import { escape } from "./utils";
|
||||||
|
import directives from "./qweb_directives";
|
||||||
|
import { Directive } from "./qweb_directives";
|
||||||
|
|
||||||
|
type RawTemplate = string;
|
||||||
|
type Template = (context: any) => DocumentFragment;
|
||||||
|
|
||||||
|
// Evaluation Context
|
||||||
|
export type EvalContext = { [key: string]: any };
|
||||||
|
|
||||||
|
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||||
|
","
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compilation Context
|
||||||
|
export class Context {
|
||||||
|
nextID: number = 1;
|
||||||
|
code: string[] = [];
|
||||||
|
variables: { [key: string]: any } = {};
|
||||||
|
escaping: boolean = false;
|
||||||
|
parentNode: string | undefined;
|
||||||
|
indentLevel: number = 0;
|
||||||
|
rootContext: Context;
|
||||||
|
caller: Element | undefined;
|
||||||
|
fragmentID: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.rootContext = this;
|
||||||
|
this.fragmentID = this.generateID();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateID(): string {
|
||||||
|
const id = `_${this.rootContext.nextID}`;
|
||||||
|
this.rootContext.nextID++;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
withParent(node: string): Context {
|
||||||
|
const newContext = Object.create(this);
|
||||||
|
newContext.parentNode = node;
|
||||||
|
return newContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
withVariables(variables: { [key: string]: any }) {
|
||||||
|
const newContext = Object.create(this);
|
||||||
|
newContext.variables = Object.create(variables);
|
||||||
|
return newContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
withCaller(node: Element): Context {
|
||||||
|
const newContext = Object.create(this);
|
||||||
|
newContext.caller = node;
|
||||||
|
return newContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
withEscaping(): Context {
|
||||||
|
const newContext = Object.create(this);
|
||||||
|
newContext.escaping = true;
|
||||||
|
return newContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
indent() {
|
||||||
|
this.indentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
dedent() {
|
||||||
|
this.indentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNode(nodeID: string) {
|
||||||
|
if (this.parentNode) {
|
||||||
|
this.addLine(`${this.parentNode}.appendChild(${nodeID})`);
|
||||||
|
} else {
|
||||||
|
this.addLine(`${this.fragmentID}.appendChild(${nodeID})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addLine(line: string) {
|
||||||
|
const prefix = new Array(this.indentLevel).join('\t');
|
||||||
|
const lastChar = line[line.length - 1];
|
||||||
|
const suffix = lastChar !=='}' && lastChar !== '{' ? ';' : '';
|
||||||
|
this.code.push(prefix + line + suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template rendering engine
|
||||||
|
*/
|
||||||
|
export default class QWeb {
|
||||||
|
rawTemplates: { [name: string]: RawTemplate } = {};
|
||||||
|
nodeTemplates: { [name: string]: Document } = {};
|
||||||
|
templates: { [name: string]: Template } = {};
|
||||||
|
escape: ((str: string) => string) = escape;
|
||||||
|
exprCache: { [key: string]: string } = {};
|
||||||
|
directives: Directive[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
directives.forEach(d => this.addDirective(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
addDirective(dir: Directive) {
|
||||||
|
this.directives.push(dir);
|
||||||
|
this.directives.sort((d1, d2) => d1.priority - d2.priority);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a template to the internal template map. Note that it is not
|
||||||
|
* immediately compiled.
|
||||||
|
*/
|
||||||
|
addTemplate(name: string, template: RawTemplate) {
|
||||||
|
if (name in this.rawTemplates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.rawTemplates[name] = template;
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(template, "text/xml");
|
||||||
|
if (!doc.firstChild) {
|
||||||
|
throw new Error("Invalid template (should not be empty)");
|
||||||
|
}
|
||||||
|
if (doc.firstChild.nodeName === "parsererror") {
|
||||||
|
throw new Error("Invalid XML in template");
|
||||||
|
}
|
||||||
|
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 = 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 nodes between branch nodes are removed
|
||||||
|
let textNode;
|
||||||
|
while ((textNode = node.previousSibling) !== prevElem) {
|
||||||
|
if (textNode.nodeValue.trim().length) {
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodeTemplates[name] = doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a template
|
||||||
|
*
|
||||||
|
* @param {string} name the template should already have been added
|
||||||
|
*/
|
||||||
|
render(name: string, context: any = {}): DocumentFragment {
|
||||||
|
const template = this.templates[name] || this._compile(name);
|
||||||
|
return template(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToString(name: string, context: EvalContext = {}): string {
|
||||||
|
const node = this.render(name, context);
|
||||||
|
return this._renderNodeToString(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderNodeToString(node: Node): string {
|
||||||
|
switch (node.nodeType) {
|
||||||
|
case 3: // text node
|
||||||
|
return node.textContent!;
|
||||||
|
case 11: // document.fragment
|
||||||
|
const children = Array.from((<DocumentFragment>node).childNodes);
|
||||||
|
return children.map(this._renderNodeToString).join("");
|
||||||
|
default:
|
||||||
|
return (<HTMLElement>node).outerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_compile(name: string): Template {
|
||||||
|
if (name in this.templates) {
|
||||||
|
return this.templates[name];
|
||||||
|
}
|
||||||
|
const doc = this.nodeTemplates[name];
|
||||||
|
|
||||||
|
let ctx = new Context();
|
||||||
|
|
||||||
|
const mainNode = doc.firstChild!;
|
||||||
|
ctx.addLine(`let ${ctx.fragmentID} = document.createDocumentFragment()`);
|
||||||
|
this._compileNode(mainNode, ctx);
|
||||||
|
|
||||||
|
ctx.addLine(`return ${ctx.fragmentID}`);
|
||||||
|
const functionCode = ctx.code.join("\n");
|
||||||
|
if ((<Element>mainNode).attributes.hasOwnProperty("t-debug")) {
|
||||||
|
console.log(
|
||||||
|
`Template: ${this.rawTemplates[name]}\nCompiled code:\n` + functionCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const template: Template = (new Function(
|
||||||
|
"context",
|
||||||
|
functionCode
|
||||||
|
) as Template).bind(this);
|
||||||
|
this.templates[name] = template;
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate code from an xml node
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_compileNode(node: ChildNode, ctx: Context) {
|
||||||
|
if (!(node instanceof Element)) {
|
||||||
|
// this is a text node, there are no directive to apply
|
||||||
|
let text = node.textContent!;
|
||||||
|
let nodeID = ctx.generateID();
|
||||||
|
ctx.addLine(`let ${nodeID} = document.createTextNode(\`${text}\`)`);
|
||||||
|
ctx.addNode(nodeID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = (<Element>node).attributes;
|
||||||
|
|
||||||
|
const validDirectives: {
|
||||||
|
directive: Directive;
|
||||||
|
value: string;
|
||||||
|
fullName: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (let directive of this.directives) {
|
||||||
|
// const value = attributes[i].textContent!;
|
||||||
|
let fullName;
|
||||||
|
let value;
|
||||||
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
|
const name = attributes[i].name;
|
||||||
|
if (
|
||||||
|
name === "t-" + directive.name ||
|
||||||
|
name.startsWith("t-" + directive.name + "-")
|
||||||
|
) {
|
||||||
|
fullName = name;
|
||||||
|
value = attributes[i].textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fullName) {
|
||||||
|
validDirectives.push({ directive, value, fullName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let { directive, value, fullName } of validDirectives) {
|
||||||
|
if (directive.atNodeEncounter) {
|
||||||
|
const isDone = directive.atNodeEncounter({
|
||||||
|
node,
|
||||||
|
qweb: this,
|
||||||
|
ctx,
|
||||||
|
fullName,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
if (isDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.nodeName !== "t") {
|
||||||
|
let nodeID = this._compileGenericNode(node, ctx);
|
||||||
|
ctx = ctx.withParent(nodeID);
|
||||||
|
|
||||||
|
for (let { directive, value, fullName } of validDirectives) {
|
||||||
|
if (directive.atNodeCreation) {
|
||||||
|
directive.atNodeCreation({
|
||||||
|
node,
|
||||||
|
qweb: this,
|
||||||
|
ctx,
|
||||||
|
fullName,
|
||||||
|
value,
|
||||||
|
nodeID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._compileChildren(node, ctx);
|
||||||
|
|
||||||
|
for (let { directive, value, fullName } of validDirectives) {
|
||||||
|
if (directive.finalize) {
|
||||||
|
directive.finalize({ node, qweb: this, ctx, fullName, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getValue(val: any, ctx: Context): any {
|
||||||
|
if (val in ctx.variables) {
|
||||||
|
return this._getValue(ctx.variables[val], ctx);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
_compileChildren(node: ChildNode, ctx: Context) {
|
||||||
|
if (node.childNodes.length > 0) {
|
||||||
|
for (let child of Array.from(node.childNodes)) {
|
||||||
|
this._compileNode(child, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_compileGenericNode(node: ChildNode, ctx: Context): string {
|
||||||
|
let nodeID: string | undefined;
|
||||||
|
switch (node.nodeType) {
|
||||||
|
case 1: // generic tag;
|
||||||
|
nodeID = ctx.generateID();
|
||||||
|
ctx.addLine(
|
||||||
|
`let ${nodeID} = document.createElement('${node.nodeName}')`
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributes = (<Element>node).attributes;
|
||||||
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
|
const name = attributes[i].name;
|
||||||
|
const value = attributes[i].textContent!;
|
||||||
|
if (!name.startsWith("t-")) {
|
||||||
|
ctx.addLine(
|
||||||
|
`${nodeID}.setAttribute('${name}', '${escape(value)}')`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (name.startsWith("t-att-")) {
|
||||||
|
const attName = name.slice(6);
|
||||||
|
const formattedValue = this._formatExpression(value!);
|
||||||
|
const attID = ctx.generateID();
|
||||||
|
ctx.addLine(`let ${attID} = ${formattedValue}`);
|
||||||
|
ctx.addLine(
|
||||||
|
`if (${attID}) {${nodeID}.setAttribute('${attName}', ${attID})}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (name.startsWith("t-attf-")) {
|
||||||
|
const exprName = name.slice(7);
|
||||||
|
const formattedExpr = value!.replace(
|
||||||
|
/\{\{.*?\}\}/g,
|
||||||
|
s => "${" + this._formatExpression(s.slice(2, -2)) + "}"
|
||||||
|
);
|
||||||
|
ctx.addLine(
|
||||||
|
`${nodeID}.setAttribute('${exprName}', \`${formattedExpr}\`)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (name === "t-att") {
|
||||||
|
const id = ctx.generateID();
|
||||||
|
ctx.addLine(`let ${id} = ${this._formatExpression(value!)}`);
|
||||||
|
ctx.addLine(`if (${id} instanceof Array) {`);
|
||||||
|
ctx.indent();
|
||||||
|
ctx.addLine(`${nodeID}.setAttribute(${id}[0], ${id}[1])`);
|
||||||
|
ctx.dedent();
|
||||||
|
ctx.addLine(`} else {`);
|
||||||
|
ctx.indent();
|
||||||
|
ctx.addLine(`for (let key in ${id}) {`);
|
||||||
|
ctx.indent();
|
||||||
|
ctx.addLine(`${nodeID}.setAttribute(key, ${id}[key])`);
|
||||||
|
ctx.dedent();
|
||||||
|
ctx.addLine(`}`);
|
||||||
|
ctx.dedent();
|
||||||
|
ctx.addLine(`}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("unknown node type");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addNode(nodeID);
|
||||||
|
return nodeID;
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatExpression(e: string): string {
|
||||||
|
if (e in this.exprCache) {
|
||||||
|
return this.exprCache[e];
|
||||||
|
}
|
||||||
|
// Thanks CHM for this code...
|
||||||
|
const chars = e.split("");
|
||||||
|
let instring = "";
|
||||||
|
let invar = "";
|
||||||
|
let invarPos = 0;
|
||||||
|
let r = "";
|
||||||
|
chars.push(" ");
|
||||||
|
for (var i = 0, ilen = chars.length; i < ilen; i++) {
|
||||||
|
var c = chars[i];
|
||||||
|
if (instring.length) {
|
||||||
|
if (c === instring && chars[i - 1] !== "\\") {
|
||||||
|
instring = "";
|
||||||
|
}
|
||||||
|
} else if (c === '"' || c === "'") {
|
||||||
|
instring = c;
|
||||||
|
} else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
|
||||||
|
invar = c;
|
||||||
|
invarPos = i;
|
||||||
|
continue;
|
||||||
|
} else if (c.match(/\W/) && invar.length) {
|
||||||
|
// TODO: Should check for possible spaces before dot
|
||||||
|
if (chars[invarPos - 1] !== "." && RESERVED_WORDS.indexOf(invar) < 0) {
|
||||||
|
invar = "context['" + invar + "']";
|
||||||
|
}
|
||||||
|
r += invar;
|
||||||
|
invar = "";
|
||||||
|
} else if (invar.length) {
|
||||||
|
invar += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
r += c;
|
||||||
|
}
|
||||||
|
const result = r.slice(0, -1);
|
||||||
|
this.exprCache[e] = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import QWeb from "./qweb";
|
||||||
|
import { Context } from "./qweb";
|
||||||
|
|
||||||
|
interface CompilationInfo {
|
||||||
|
nodeID?: string;
|
||||||
|
node: Element;
|
||||||
|
qweb: QWeb;
|
||||||
|
ctx: Context;
|
||||||
|
fullName: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Directive {
|
||||||
|
name: 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;
|
||||||
|
atNodeCreation?(info: CompilationInfo);
|
||||||
|
finalize?(info: CompilationInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forEachDirective: Directive = {
|
||||||
|
name: "foreach",
|
||||||
|
priority: 10,
|
||||||
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
|
const elems = node.getAttribute("t-foreach")!;
|
||||||
|
const name = node.getAttribute("t-as")!;
|
||||||
|
let arrayID = ctx.generateID();
|
||||||
|
ctx.addLine(`let ${arrayID} = ${qweb._formatExpression(elems)}`);
|
||||||
|
ctx.addLine(
|
||||||
|
`if (typeof ${arrayID} === 'number') { ${arrayID} = Array.from(Array(${arrayID}).keys())}`
|
||||||
|
);
|
||||||
|
let keysID = ctx.generateID();
|
||||||
|
ctx.addLine(
|
||||||
|
`let ${keysID} = ${arrayID} instanceof Array ? ${arrayID} : Object.keys(${arrayID})`
|
||||||
|
);
|
||||||
|
let valuesID = ctx.generateID();
|
||||||
|
ctx.addLine(
|
||||||
|
`let ${valuesID} = ${arrayID} instanceof Array ? ${arrayID} : Object.values(${arrayID})`
|
||||||
|
);
|
||||||
|
ctx.addLine(`for (let i = 0; i < ${keysID}.length; i++) {`);
|
||||||
|
ctx.indent();
|
||||||
|
ctx.addLine(`context.${name}_first = i === 0`);
|
||||||
|
ctx.addLine(`context.${name}_last = i === ${keysID}.length - 1`);
|
||||||
|
ctx.addLine(`context.${name}_parity = i % 2 === 0 ? 'even' : 'odd'`);
|
||||||
|
ctx.addLine(`context.${name}_index = i`);
|
||||||
|
ctx.addLine(`context.${name} = ${keysID}[i]`);
|
||||||
|
ctx.addLine(`context.${name}_value = ${valuesID}[i]`);
|
||||||
|
const nodes = Array.from(node.childNodes);
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
qweb._compileNode(nodes[i], ctx);
|
||||||
|
}
|
||||||
|
ctx.dedent();
|
||||||
|
ctx.addLine("}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ifDirective: Directive = {
|
||||||
|
name: "if",
|
||||||
|
priority: 20,
|
||||||
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
|
let cond = qweb._getValue(node.getAttribute("t-if")!, ctx);
|
||||||
|
ctx.addLine(`if (${qweb._formatExpression(cond)}) {`);
|
||||||
|
ctx.indent();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
finalize({ ctx }) {
|
||||||
|
ctx.dedent();
|
||||||
|
ctx.addLine(`}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const elifDirective: Directive = {
|
||||||
|
name: "elif",
|
||||||
|
priority: 30,
|
||||||
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
|
let cond = qweb._getValue(node.getAttribute("t-elif")!, ctx);
|
||||||
|
ctx.addLine(`else if (${qweb._formatExpression(cond)}) {`);
|
||||||
|
ctx.indent();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
finalize({ ctx }) {
|
||||||
|
ctx.dedent();
|
||||||
|
ctx.addLine(`}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const elseDirective: Directive = {
|
||||||
|
name: "else",
|
||||||
|
priority: 40,
|
||||||
|
atNodeEncounter({ ctx }): boolean {
|
||||||
|
ctx.addLine(`else {`);
|
||||||
|
ctx.indent();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
finalize({ ctx }) {
|
||||||
|
ctx.dedent();
|
||||||
|
ctx.addLine(`}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const callDirective: Directive = {
|
||||||
|
name: "call",
|
||||||
|
priority: 50,
|
||||||
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
|
const subTemplate = node.getAttribute("t-call")!;
|
||||||
|
const nodeTemplate = qweb.nodeTemplates[subTemplate];
|
||||||
|
const nodeCopy = <Element>node.cloneNode(true);
|
||||||
|
nodeCopy.removeAttribute("t-call");
|
||||||
|
|
||||||
|
// extract variables from nodecopy
|
||||||
|
const tempCtx = new Context();
|
||||||
|
qweb._compileNode(nodeCopy, tempCtx);
|
||||||
|
const vars = Object.assign({}, ctx.variables, tempCtx.variables);
|
||||||
|
const subCtx = ctx.withCaller(nodeCopy).withVariables(vars);
|
||||||
|
|
||||||
|
qweb._compileNode(nodeTemplate.firstChild!, subCtx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setDirective: Directive = {
|
||||||
|
name: "set",
|
||||||
|
priority: 60,
|
||||||
|
atNodeEncounter({ node, ctx }): boolean {
|
||||||
|
const variable = node.getAttribute("t-set")!;
|
||||||
|
let value = node.getAttribute("t-value")!;
|
||||||
|
if (value) {
|
||||||
|
ctx.variables[variable] = value;
|
||||||
|
} else {
|
||||||
|
ctx.variables[variable] = node.childNodes;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Context) {
|
||||||
|
if (value === "0" && ctx.caller) {
|
||||||
|
qweb._compileNode(ctx.caller, ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const exprID = ctx.generateID();
|
||||||
|
ctx.addLine(`let ${exprID} = ${qweb._formatExpression(value)}`);
|
||||||
|
ctx.addLine(`if (${exprID} || ${exprID} === 0) {`);
|
||||||
|
ctx.indent();
|
||||||
|
let text = exprID;
|
||||||
|
if (ctx.escaping) {
|
||||||
|
text = `this.escape(${text})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeID = ctx.generateID();
|
||||||
|
ctx.addLine(`let ${nodeID} = document.createTextNode(${text})`);
|
||||||
|
ctx.addNode(nodeID);
|
||||||
|
ctx.dedent();
|
||||||
|
if (node.childNodes.length) {
|
||||||
|
ctx.addLine("} else {");
|
||||||
|
ctx.indent();
|
||||||
|
qweb._compileChildren(node, ctx);
|
||||||
|
ctx.dedent();
|
||||||
|
}
|
||||||
|
ctx.addLine("}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value instanceof NodeList) {
|
||||||
|
for (let node of Array.from(value)) {
|
||||||
|
qweb._compileNode(<ChildNode>node, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const escDirective: Directive = {
|
||||||
|
name: "esc",
|
||||||
|
priority: 70,
|
||||||
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
|
if (node.nodeName !== "t") {
|
||||||
|
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||||
|
ctx = ctx.withParent(nodeID);
|
||||||
|
}
|
||||||
|
let value = qweb._getValue(node.getAttribute("t-esc")!, ctx);
|
||||||
|
compileValueNode(value, node, qweb, ctx.withEscaping());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawDirective: Directive = {
|
||||||
|
name: "raw",
|
||||||
|
priority: 80,
|
||||||
|
atNodeEncounter({ node, qweb, ctx }): boolean {
|
||||||
|
if (node.nodeName !== "t") {
|
||||||
|
let nodeID = qweb._compileGenericNode(node, ctx);
|
||||||
|
ctx = ctx.withParent(nodeID);
|
||||||
|
}
|
||||||
|
let value = qweb._getValue(node.getAttribute("t-raw")!, ctx);
|
||||||
|
compileValueNode(value, node, qweb, ctx);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDirective: Directive = {
|
||||||
|
name: "on",
|
||||||
|
priority: 90,
|
||||||
|
atNodeCreation({ ctx, fullName, value, nodeID }) {
|
||||||
|
const eventName = fullName.slice(5);
|
||||||
|
let extraArgs;
|
||||||
|
let handler = value.replace(/\(.*\)/, function(args) {
|
||||||
|
extraArgs = args.slice(1, -1);
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
ctx.addLine(
|
||||||
|
`${nodeID}.addEventListener('${eventName}', context['${handler}'].bind(context${
|
||||||
|
extraArgs ? ", " + extraArgs : ""
|
||||||
|
}))`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [
|
||||||
|
forEachDirective,
|
||||||
|
ifDirective,
|
||||||
|
elifDirective,
|
||||||
|
elseDirective,
|
||||||
|
callDirective,
|
||||||
|
setDirective,
|
||||||
|
escDirective,
|
||||||
|
rawDirective,
|
||||||
|
onDirective
|
||||||
|
];
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export function escape(str: string | number | undefined): string {
|
||||||
|
if (str === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof str === "number") {
|
||||||
|
return String(str);
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, "'")
|
||||||
|
.replace(/`/g, "`");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove trailing and leading spaces
|
||||||
|
*/
|
||||||
|
export function htmlTrim(s: string): string {
|
||||||
|
let result = s.replace(/(^\s+|\s+$)/g, "");
|
||||||
|
if (s[0] === ' ') {
|
||||||
|
result = ' ' + result;
|
||||||
|
}
|
||||||
|
if (result !== ' ' && s[s.length - 1] === ' ') {
|
||||||
|
result = result + ' ';
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,511 @@
|
|||||||
|
import QWeb from "../src/qweb";
|
||||||
|
import { EvalContext } from "../src/qweb";
|
||||||
|
|
||||||
|
|
||||||
|
function renderToDOM(t: string, context: EvalContext = {}): DocumentFragment {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("test", t);
|
||||||
|
return qweb.render("test", context);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToString(t: string, context: EvalContext = {}): string {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("test", t);
|
||||||
|
return qweb.renderToString("test", context);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("static templates", () => {
|
||||||
|
test("minimal template", () => {
|
||||||
|
const template = "<t>ok</t>";
|
||||||
|
const expected = "ok";
|
||||||
|
expect(renderToString(template)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty div", () => {
|
||||||
|
const template = "<div></div>";
|
||||||
|
const expected = template;
|
||||||
|
expect(renderToString(template)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("div with a text node", () => {
|
||||||
|
const template = "<div>word</div>";
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe(template);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("div with a span child node", () => {
|
||||||
|
const template = "<div><span>word</span></div>";
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe(template);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("error handling", () => {
|
||||||
|
test("invalid xml", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
|
||||||
|
expect(() => qweb.addTemplate("test", "<div>")).toThrow(
|
||||||
|
"Invalid XML in template"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-esc", () => {
|
||||||
|
test("literal", () => {
|
||||||
|
const template = `<t t-esc="'ok'"/>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("variable", () => {
|
||||||
|
const template = `<t t-esc="var"/>`;
|
||||||
|
const result = renderToString(template, { var: "ok" });
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("escaping", () => {
|
||||||
|
const template = `<t t-esc="var"/>`;
|
||||||
|
const result = renderToString(template, { var: "<ok>" });
|
||||||
|
expect(result).toBe("<ok>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("escaping on a node", () => {
|
||||||
|
const template = `<span t-esc="'ok'"/>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("<span>ok</span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("escaping on a node with a body", () => {
|
||||||
|
const template = `<span t-esc="'ok'">nope</span>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("<span>ok</span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("escaping on a node with a body, as a default", () => {
|
||||||
|
const template = `<span t-esc="var">nope</span>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("<span>nope</span>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-raw", () => {
|
||||||
|
test("literal", () => {
|
||||||
|
const template = `<t t-raw="'ok'"/>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("variable", () => {
|
||||||
|
const template = `<t t-raw="var"/>`;
|
||||||
|
const result = renderToString(template, { var: "ok" });
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("not escaping", () => {
|
||||||
|
const template = `<t t-raw="var"/>`;
|
||||||
|
const result = renderToString(template, { var: "<ok>" });
|
||||||
|
expect(result).toBe("<ok>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-set", () => {
|
||||||
|
test("set from attribute literal", () => {
|
||||||
|
const template = `<t><t t-set="value" t-value="'ok'"/><t t-esc="value"/></t>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set from body literal", () => {
|
||||||
|
const template = `<t><t t-set="value">ok</t><t t-esc="value"/></t>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set from attribute lookup", () => {
|
||||||
|
const template = `<t><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></t>`;
|
||||||
|
const result = renderToString(template, { value: "ok" });
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set from body lookup", () => {
|
||||||
|
const template = `<t><t t-set="stuff"><t t-esc="value"/></t><t t-esc="stuff"/></t>`;
|
||||||
|
const result = renderToString(template, { value: "ok" });
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("set from empty body", () => {
|
||||||
|
const template = `<t><t t-set="stuff"/><t t-esc="stuff"/></t>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("value priority", () => {
|
||||||
|
const template = `<t><t t-set="value" t-value="1">2</t><t t-esc="value"/></t>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("evaluate value expression", () => {
|
||||||
|
const template = `<t><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></t>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("3");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("evaluate value expression, part 2", () => {
|
||||||
|
const template = `<t><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></t>`;
|
||||||
|
const result = renderToString(template, { somevariable: 43 });
|
||||||
|
expect(result).toBe("45");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-if", () => {
|
||||||
|
test("boolean value true condition", () => {
|
||||||
|
const template = `<t t-if="condition">ok</t>`;
|
||||||
|
const result = renderToString(template, { condition: true });
|
||||||
|
expect(result).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("boolean value false condition", () => {
|
||||||
|
const template = `<t t-if="condition">fail</t>`;
|
||||||
|
const result = renderToString(template, { condition: false });
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("boolean value condition missing", () => {
|
||||||
|
const template = `<t t-if="condition">fail</t>`;
|
||||||
|
const result = renderToString(template);
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("boolean value condition elif", () => {
|
||||||
|
const template = `
|
||||||
|
<t>
|
||||||
|
<t t-if="color == 'black'">black pearl</t>
|
||||||
|
<t t-elif="color == 'yellow'">yellow submarine</t>
|
||||||
|
<t t-elif="color == 'red'">red is dead</t>
|
||||||
|
<t t-else="">beer</t>
|
||||||
|
</t>
|
||||||
|
`;
|
||||||
|
const result = renderToString(template, { color: "red" });
|
||||||
|
expect(result.trim()).toBe("red is dead");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("boolean value condition else", () => {
|
||||||
|
const template = `
|
||||||
|
<div>
|
||||||
|
<span>begin</span>
|
||||||
|
<t t-if="condition">ok</t>
|
||||||
|
<t t-else="">ok-else</t>
|
||||||
|
<span>end</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const result = renderToString(template, { condition: true });
|
||||||
|
expect(result).toBe(
|
||||||
|
"<div>\n <span>begin</span>\n ok\n <span>end</span>\n </div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("boolean value condition false else", () => {
|
||||||
|
const template = `
|
||||||
|
<div><span>begin</span><t t-if="condition">fail</t>
|
||||||
|
<t t-else="">fail-else</t><span>end</span></div>
|
||||||
|
`;
|
||||||
|
const result = renderToString(template, { condition: false });
|
||||||
|
expect(result).toBe(
|
||||||
|
"<div><span>begin</span>fail-else<span>end</span></div>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("attributes", () => {
|
||||||
|
test("static attributes", () => {
|
||||||
|
const template = `<div foo="a" bar="b" baz="c"/>`;
|
||||||
|
const expected = `<div foo="a" bar="b" baz="c"></div>`;
|
||||||
|
expect(renderToString(template)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("static attributes on void elements", () => {
|
||||||
|
const template = `<img src="/test.jpg" alt="Test"/>`;
|
||||||
|
const expected = `<img src="/test.jpg" alt="Test">`;
|
||||||
|
expect(renderToString(template)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dynamic attributes", () => {
|
||||||
|
const template = `<div t-att-foo="'bar'"/>`;
|
||||||
|
const expected = `<div foo="bar"></div>`;
|
||||||
|
expect(renderToString(template)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fixed variable", () => {
|
||||||
|
const template = `<div t-att-foo="value"/>`;
|
||||||
|
const expected = `<div foo="ok"></div>`;
|
||||||
|
expect(renderToString(template, { value: "ok" })).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dynamic attribute falsy variable ", () => {
|
||||||
|
const template = `<div t-att-foo="value"/>`;
|
||||||
|
const expected = `<div></div>`;
|
||||||
|
expect(renderToString(template, { value: false })).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tuple literal", () => {
|
||||||
|
const template = `<div t-att="['foo', 'bar']"/>`;
|
||||||
|
const expected = `<div foo="bar"></div>`;
|
||||||
|
expect(renderToString(template)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tuple variable", () => {
|
||||||
|
const template = `<div t-att="value"/>`;
|
||||||
|
const expected = `<div foo="bar"></div>`;
|
||||||
|
expect(renderToString(template, { value: ["foo", "bar"] })).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("object", () => {
|
||||||
|
const template = `<div t-att="value"/>`;
|
||||||
|
const expected = `<div a="1" b="2" c="3"></div>`;
|
||||||
|
expect(renderToString(template, { value: { a: 1, b: 2, c: 3 } })).toBe(
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("format literal", () => {
|
||||||
|
const template = `<div t-attf-foo="bar"/>`;
|
||||||
|
const expected = `<div foo="bar"></div>`;
|
||||||
|
expect(renderToString(template)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("format value", () => {
|
||||||
|
const template = `<div t-attf-foo="b{{value}}r"/>`;
|
||||||
|
const expected = `<div foo="bar"></div>`;
|
||||||
|
expect(renderToString(template, { value: "a" })).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("format expression", () => {
|
||||||
|
const template = `<div t-attf-foo="{{value + 37}}"/>`;
|
||||||
|
const expected = `<div foo="42"></div>`;
|
||||||
|
expect(renderToString(template, { value: 5 })).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("format multiple", () => {
|
||||||
|
const template = `<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>`;
|
||||||
|
const expected = `<div foo="a 0 is 1 of 2 ]"></div>`;
|
||||||
|
expect(renderToString(template, { value1: 0, value2: 1, value3: 2 })).toBe(
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
xit("various escapes", () => {
|
||||||
|
// need to think about this... This one does not pass, but I am not sure it is
|
||||||
|
// a correct test
|
||||||
|
const template = `
|
||||||
|
<div foo="<foo"
|
||||||
|
t-att-bar="bar"
|
||||||
|
t-attf-baz="<{{baz}}>"
|
||||||
|
t-att="qux"/>
|
||||||
|
`;
|
||||||
|
const expected = `<div foo="<foo" bar="<bar>" baz="<"<baz>">" qux="<>"></div>`;
|
||||||
|
expect(
|
||||||
|
renderToString(template, { bar: 0, baz: 1, qux: { qux: "<>" } })
|
||||||
|
).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-call (template calling", () => {
|
||||||
|
test("basic caller", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_basic-callee", "<t>ok</t>");
|
||||||
|
qweb.addTemplate("caller", '<t t-call="_basic-callee"/>');
|
||||||
|
const expected = "ok";
|
||||||
|
expect(qweb.renderToString("caller")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with unused body", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_basic-callee", "<t>ok</t>");
|
||||||
|
qweb.addTemplate("caller", '<t t-call="_basic-callee">WHEEE</t>');
|
||||||
|
const expected = "ok";
|
||||||
|
expect(qweb.renderToString("caller")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with unused setbody", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_basic-callee", "<t>ok</t>");
|
||||||
|
qweb.addTemplate(
|
||||||
|
"caller",
|
||||||
|
'<t t-call="_basic-callee"><t t-set="qux" t-value="3"/></t>'
|
||||||
|
);
|
||||||
|
const expected = "ok";
|
||||||
|
expect(qweb.renderToString("caller")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with used body", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_callee-printsbody", '<t t-esc="0"/>');
|
||||||
|
qweb.addTemplate("caller", '<t t-call="_callee-printsbody">ok</t>');
|
||||||
|
const expected = "ok";
|
||||||
|
expect(qweb.renderToString("caller")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with used set body", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_callee-uses-foo", '<t t-esc="foo"/>');
|
||||||
|
qweb.addTemplate(
|
||||||
|
"caller",
|
||||||
|
`
|
||||||
|
<t t-call="_callee-uses-foo"><t t-set="foo" t-value="'ok'"/></t>`
|
||||||
|
);
|
||||||
|
const expected = "ok";
|
||||||
|
expect(qweb.renderToString("caller")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("inherit context", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_callee-uses-foo", '<t t-esc="foo"/>');
|
||||||
|
qweb.addTemplate(
|
||||||
|
"caller",
|
||||||
|
`
|
||||||
|
<t><t t-set="foo" t-value="1"/><t t-call="_callee-uses-foo"/></t>`
|
||||||
|
);
|
||||||
|
const expected = "1";
|
||||||
|
expect(qweb.renderToString("caller")).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("scoped parameters", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_basic-callee", `<t t-name="_basic-callee">ok</t>`);
|
||||||
|
qweb.addTemplate(
|
||||||
|
"caller",
|
||||||
|
`
|
||||||
|
<t>
|
||||||
|
<t t-call="_basic-callee">
|
||||||
|
<t t-set="foo" t-value="42"/>
|
||||||
|
</t>
|
||||||
|
<t t-esc="foo"/>
|
||||||
|
</t>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const expected = "ok";
|
||||||
|
expect(qweb.renderToString("caller").trim()).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("foreach", () => {
|
||||||
|
test("iterate on items", () => {
|
||||||
|
const template = `<t t-foreach="[3, 2, 1]" t-as="item">
|
||||||
|
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||||
|
</t>`;
|
||||||
|
const expected = `[0: 3 3]\n \n [1: 2 2]\n \n [2: 1 1]`;
|
||||||
|
expect(renderToString(template).trim()).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("iterate, position", () => {
|
||||||
|
const template = `<t t-foreach="5" t-as="elem">
|
||||||
|
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_parity"/>)
|
||||||
|
</t>`;
|
||||||
|
const expected = `- first (even)\n \n - (odd)\n \n - (even)\n \n - (odd)\n \n - last (even)`;
|
||||||
|
expect(renderToString(template).trim()).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("iterate, integer param", () => {
|
||||||
|
const template = `<t t-foreach="3" t-as="item">
|
||||||
|
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
|
||||||
|
</t>`;
|
||||||
|
const expected = `[0: 0 0]\n \n [1: 1 1]\n \n [2: 2 2]`;
|
||||||
|
expect(renderToString(template).trim()).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("iterate, dict param", () => {
|
||||||
|
const template = `<t t-foreach="value" t-as="item">
|
||||||
|
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]</t>`;
|
||||||
|
const expected = `[0: a 1 - even]\n [1: b 2 - odd]\n [2: c 3 - even]`;
|
||||||
|
expect(
|
||||||
|
renderToString(template, { value: { a: 1, b: 2, c: 3 } }).trim()
|
||||||
|
).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("misc", () => {
|
||||||
|
test("global", () => {
|
||||||
|
const qweb = new QWeb();
|
||||||
|
qweb.addTemplate("_callee-asc", `<Año t-att-falló="'agüero'" t-raw="0"/>`);
|
||||||
|
qweb.addTemplate(
|
||||||
|
"_callee-uses-foo",
|
||||||
|
`<span t-esc="foo">foo default</span>`
|
||||||
|
);
|
||||||
|
qweb.addTemplate(
|
||||||
|
"_callee-asc-toto",
|
||||||
|
`<div t-raw="toto">toto default</div>`
|
||||||
|
);
|
||||||
|
qweb.addTemplate(
|
||||||
|
"caller",
|
||||||
|
`
|
||||||
|
<t>
|
||||||
|
<t t-foreach="[4,5,6]" t-as="value">
|
||||||
|
<span t-esc="value"/>
|
||||||
|
<t t-call="_callee-asc">
|
||||||
|
<t t-call="_callee-uses-foo">
|
||||||
|
<t t-set="foo" t-value="'aaa'"/>
|
||||||
|
</t>
|
||||||
|
<t t-call="_callee-uses-foo"/>
|
||||||
|
<t t-set="foo" t-value="'bbb'"/>
|
||||||
|
<t t-call="_callee-uses-foo"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<t t-call="_callee-asc-toto"/>
|
||||||
|
</t>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const expected = `
|
||||||
|
<span>4</span>
|
||||||
|
<año falló="agüero">
|
||||||
|
<span>aaa</span>
|
||||||
|
<span>foo default</span>
|
||||||
|
|
||||||
|
<span>bbb</span>
|
||||||
|
</año>
|
||||||
|
|
||||||
|
<span>5</span>
|
||||||
|
<año falló="agüero">
|
||||||
|
<span>aaa</span>
|
||||||
|
<span>foo default</span>
|
||||||
|
|
||||||
|
<span>bbb</span>
|
||||||
|
</año>
|
||||||
|
|
||||||
|
<span>6</span>
|
||||||
|
<año falló="agüero">
|
||||||
|
<span>aaa</span>
|
||||||
|
<span>foo default</span>
|
||||||
|
|
||||||
|
<span>bbb</span>
|
||||||
|
</año>
|
||||||
|
|
||||||
|
<div>toto default</div>
|
||||||
|
|
||||||
|
`.trim();
|
||||||
|
expect(qweb.renderToString("caller").trim()).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-on", () => {
|
||||||
|
test("can bind event handler", () => {
|
||||||
|
let a = 1;
|
||||||
|
const template = `<button t-on-click="add">Click</button>`;
|
||||||
|
const fragment = renderToDOM(template, {
|
||||||
|
add() { a = 3}
|
||||||
|
});
|
||||||
|
(<HTMLElement>fragment.firstChild).click();
|
||||||
|
expect(a).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can bind handlers with arguments", () => {
|
||||||
|
let a = 1;
|
||||||
|
const template = `<button t-on-click="add(5)">Click</button>`;
|
||||||
|
const fragment = renderToDOM(template, {
|
||||||
|
add(n) { a = a + n}
|
||||||
|
});
|
||||||
|
(<HTMLElement>fragment.firstChild).click();
|
||||||
|
expect(a).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { escape, htmlTrim } from "../src/utils";
|
||||||
|
|
||||||
|
describe("escape", () => {
|
||||||
|
test("normal strings", () => {
|
||||||
|
const text = "abc";
|
||||||
|
expect(escape(text)).toBe(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("special symbols", () => {
|
||||||
|
const text = "<ok>";
|
||||||
|
expect(escape(text)).toBe("<ok>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("htmlTrim", () => {
|
||||||
|
test("basic use", () => {
|
||||||
|
expect(htmlTrim("abc")).toBe("abc");
|
||||||
|
expect(htmlTrim(" abc")).toBe(" abc");
|
||||||
|
expect(htmlTrim("abc ")).toBe("abc ");
|
||||||
|
expect(htmlTrim(" abc ")).toBe(" abc ");
|
||||||
|
expect(htmlTrim("abc\n ")).toBe("abc ");
|
||||||
|
expect(htmlTrim("\n ")).toBe(" ");
|
||||||
|
expect(htmlTrim(" \n ")).toBe(" ");
|
||||||
|
expect(htmlTrim(" ")).toBe(" ");
|
||||||
|
expect(htmlTrim("")).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"lib": ["es2015", "dom"],
|
||||||
|
"removeComments": false,
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"target": "es6",
|
||||||
|
"outDir": "dist",
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"strictNullChecks": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "tests/**/*.ts", "examples/**/*.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user