diff --git a/src/core/qweb.ts b/src/core/qweb.ts deleted file mode 100644 index 92c6f1b4..00000000 --- a/src/core/qweb.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { escape } from "./utils"; -import directives from "./qweb_directives"; -import { Directive } from "./qweb_directives"; -import {init} from "../libs/snabbdom/src/snabbdom" -import sdProps from "../libs/snabbdom/src/modules/props" -import sdListeners from "../libs/snabbdom/src/modules/eventlisteners" -import h from "../libs/snabbdom/src/h" -// const init = require("../libs/snabbdom/dist/snabbdom.js"); -// import {init} from '../libs/snabbdom/dist/snabbdom.js'; - -const patch = init([sdProps, sdListeners]); -(window).h = h; -(window).patch = patch; - - -// console.log(init); -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; - indentLevel: number = 0; - rootContext: Context; - caller: Element | undefined; - fragmentID: string; - - constructor() { - this.rootContext = this; - this.fragmentID = this.generateID(); - this.parentNode = this.fragmentID; - } - - 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) { - this.addLine(`${this.parentNode}.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); - } - getValue(val: any): any { - return val in this.variables ? this.getValue(this.variables[val]) : val; - } -} - -/** - * 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((node).childNodes); - return children.map(this._renderNodeToString).join(""); - default: - return (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 ((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 = (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 }); - } - } - } - - _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 = (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; - } -} diff --git a/src/core/qweb_directives.ts b/src/core/qweb_directives.ts deleted file mode 100644 index e604c421..00000000 --- a/src/core/qweb_directives.ts +++ /dev/null @@ -1,251 +0,0 @@ -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 = ctx.getValue(node.getAttribute("t-if")!); - 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 = ctx.getValue(node.getAttribute("t-elif")!); - 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 = 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(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 = ctx.getValue(node.getAttribute("t-esc")!); - 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 = ctx.getValue(node.getAttribute("t-raw")!); - 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 : "" - }))` - ); - } -}; - -const widgetDirective: Directive = { - name: "widget", - priority: 100, - atNodeEncounter({ ctx, fullName, value, node, qweb}): boolean { - let spanID = ctx.generateID(); - ctx.addLine(`let ${spanID} = document.createElement('span')`); - ctx.addNode(spanID) - let props = node.getAttribute('t-props'); - let widgetID = ctx.generateID(); - ctx.addLine(`let ${widgetID} = new context.widgets['${value}'](context, ${props})`); - ctx.addLine(`${widgetID}.mount(${spanID}).then(()=>${ctx.parentNode}.replaceChild(${widgetID}.el, ${spanID}))`); - let ref = node.getAttribute('t-ref'); - if (ref) { - ctx.addLine(`context.refs['${ref}'] = ${widgetID}`); - } - return true; - } -}; - -export default [ - forEachDirective, - ifDirective, - elifDirective, - elseDirective, - callDirective, - setDirective, - escDirective, - rawDirective, - onDirective, - widgetDirective -]; diff --git a/src/core/qweb_vdom.ts b/src/core/qweb_vdom.ts index 07e12dce..e33fdbba 100644 --- a/src/core/qweb_vdom.ts +++ b/src/core/qweb_vdom.ts @@ -640,3 +640,22 @@ const onDirective: Directive = { ); } }; + +// const widgetDirective: Directive = { +// name: "widget", +// priority: 100, +// atNodeEncounter({ ctx, fullName, value, node, qweb}): boolean { +// let spanID = ctx.generateID(); +// ctx.addLine(`let ${spanID} = document.createElement('span')`); +// ctx.addNode(spanID) +// let props = node.getAttribute('t-props'); +// let widgetID = ctx.generateID(); +// ctx.addLine(`let ${widgetID} = new context.widgets['${value}'](context, ${props})`); +// ctx.addLine(`${widgetID}.mount(${spanID}).then(()=>${ctx.parentNode}.replaceChild(${widgetID}.el, ${spanID}))`); +// let ref = node.getAttribute('t-ref'); +// if (ref) { +// ctx.addLine(`context.refs['${ref}'] = ${widgetID}`); +// } +// return true; +// } +// }; diff --git a/src/prout.d.ts b/src/prout.d.ts deleted file mode 100644 index 39b91804..00000000 --- a/src/prout.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module "snabbdom-to-html" diff --git a/tests/qweb.test.ts b/tests/qweb.test.ts deleted file mode 100644 index 8fa64620..00000000 --- a/tests/qweb.test.ts +++ /dev/null @@ -1,511 +0,0 @@ -import QWeb from "../src/core/qweb"; -import { EvalContext } from "../src/core/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 = "ok"; - const expected = "ok"; - expect(renderToString(template)).toBe(expected); - }); - - test("empty div", () => { - const template = "
"; - const expected = template; - expect(renderToString(template)).toBe(expected); - }); - - test("div with a text node", () => { - const template = "
word
"; - const result = renderToString(template); - expect(result).toBe(template); - }); - - test("div with a span child node", () => { - const template = "
word
"; - const result = renderToString(template); - expect(result).toBe(template); - }); -}); - -describe("error handling", () => { - test("invalid xml", () => { - const qweb = new QWeb(); - - expect(() => qweb.addTemplate("test", "
")).toThrow( - "Invalid XML in template" - ); - }); -}); - -describe("t-esc", () => { - test("literal", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("ok"); - }); - - test("variable", () => { - const template = ``; - const result = renderToString(template, { var: "ok" }); - expect(result).toBe("ok"); - }); - - test("escaping", () => { - const template = ``; - const result = renderToString(template, { var: "" }); - expect(result).toBe("<ok>"); - }); - - test("escaping on a node", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("ok"); - }); - - test("escaping on a node with a body", () => { - const template = `nope`; - const result = renderToString(template); - expect(result).toBe("ok"); - }); - - test("escaping on a node with a body, as a default", () => { - const template = `nope`; - const result = renderToString(template); - expect(result).toBe("nope"); - }); -}); - -describe("t-raw", () => { - test("literal", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("ok"); - }); - - test("variable", () => { - const template = ``; - const result = renderToString(template, { var: "ok" }); - expect(result).toBe("ok"); - }); - - test("not escaping", () => { - const template = ``; - const result = renderToString(template, { var: "" }); - expect(result).toBe(""); - }); -}); - -describe("t-set", () => { - test("set from attribute literal", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("ok"); - }); - - test("set from body literal", () => { - const template = `ok`; - const result = renderToString(template); - expect(result).toBe("ok"); - }); - - test("set from attribute lookup", () => { - const template = ``; - const result = renderToString(template, { value: "ok" }); - expect(result).toBe("ok"); - }); - - test("set from body lookup", () => { - const template = ``; - const result = renderToString(template, { value: "ok" }); - expect(result).toBe("ok"); - }); - - test("set from empty body", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe(""); - }); - - test("value priority", () => { - const template = `2`; - const result = renderToString(template); - expect(result).toBe("1"); - }); - - test("evaluate value expression", () => { - const template = ``; - const result = renderToString(template); - expect(result).toBe("3"); - }); - - test("evaluate value expression, part 2", () => { - const template = ``; - const result = renderToString(template, { somevariable: 43 }); - expect(result).toBe("45"); - }); -}); - -describe("t-if", () => { - test("boolean value true condition", () => { - const template = `ok`; - const result = renderToString(template, { condition: true }); - expect(result).toBe("ok"); - }); - - test("boolean value false condition", () => { - const template = `fail`; - const result = renderToString(template, { condition: false }); - expect(result).toBe(""); - }); - - test("boolean value condition missing", () => { - const template = `fail`; - const result = renderToString(template); - expect(result).toBe(""); - }); - - test("boolean value condition elif", () => { - const template = ` - - black pearl - yellow submarine - red is dead - beer - - `; - const result = renderToString(template, { color: "red" }); - expect(result.trim()).toBe("red is dead"); - }); - - test("boolean value condition else", () => { - const template = ` -
- begin - ok - ok-else - end -
- `; - const result = renderToString(template, { condition: true }); - expect(result).toBe( - "
\n begin\n ok\n end\n
" - ); - }); - - test("boolean value condition false else", () => { - const template = ` -
beginfail - fail-elseend
- `; - const result = renderToString(template, { condition: false }); - expect(result).toBe( - "
beginfail-elseend
" - ); - }); -}); - -describe("attributes", () => { - test("static attributes", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template)).toBe(expected); - }); - - test("static attributes on void elements", () => { - const template = `Test`; - const expected = `Test`; - expect(renderToString(template)).toBe(expected); - }); - - test("dynamic attributes", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template)).toBe(expected); - }); - - test("fixed variable", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: "ok" })).toBe(expected); - }); - - test("dynamic attribute falsy variable ", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: false })).toBe(expected); - }); - - test("tuple literal", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template)).toBe(expected); - }); - - test("tuple variable", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: ["foo", "bar"] })).toBe(expected); - }); - - test("object", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: { a: 1, b: 2, c: 3 } })).toBe( - expected - ); - }); - - test("format literal", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template)).toBe(expected); - }); - - test("format value", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: "a" })).toBe(expected); - }); - - test("format expression", () => { - const template = `
`; - const expected = `
`; - expect(renderToString(template, { value: 5 })).toBe(expected); - }); - - test("format multiple", () => { - const template = `
`; - const expected = `
`; - 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 = ` -
- `; - const expected = `
`; - 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", "ok"); - qweb.addTemplate("caller", ''); - const expected = "ok"; - expect(qweb.renderToString("caller")).toBe(expected); - }); - - test("with unused body", () => { - const qweb = new QWeb(); - qweb.addTemplate("_basic-callee", "ok"); - qweb.addTemplate("caller", 'WHEEE'); - const expected = "ok"; - expect(qweb.renderToString("caller")).toBe(expected); - }); - - test("with unused setbody", () => { - const qweb = new QWeb(); - qweb.addTemplate("_basic-callee", "ok"); - qweb.addTemplate( - "caller", - '' - ); - const expected = "ok"; - expect(qweb.renderToString("caller")).toBe(expected); - }); - - test("with used body", () => { - const qweb = new QWeb(); - qweb.addTemplate("_callee-printsbody", ''); - qweb.addTemplate("caller", 'ok'); - const expected = "ok"; - expect(qweb.renderToString("caller")).toBe(expected); - }); - - test("with used set body", () => { - const qweb = new QWeb(); - qweb.addTemplate("_callee-uses-foo", ''); - qweb.addTemplate( - "caller", - ` - ` - ); - const expected = "ok"; - expect(qweb.renderToString("caller")).toBe(expected); - }); - - test("inherit context", () => { - const qweb = new QWeb(); - qweb.addTemplate("_callee-uses-foo", ''); - qweb.addTemplate( - "caller", - ` - ` - ); - const expected = "1"; - expect(qweb.renderToString("caller")).toBe(expected); - }); - - test("scoped parameters", () => { - const qweb = new QWeb(); - qweb.addTemplate("_basic-callee", `ok`); - qweb.addTemplate( - "caller", - ` - - - - - - - ` - ); - const expected = "ok"; - expect(qweb.renderToString("caller").trim()).toBe(expected); - }); -}); - -describe("foreach", () => { - test("iterate on items", () => { - const template = ` - [: ] - `; - 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 = ` - - first last () - `; - 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 = ` - [: ] - `; - 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 = ` - [: - ]`; - 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", ``); - qweb.addTemplate( - "_callee-uses-foo", - `foo default` - ); - qweb.addTemplate( - "_callee-asc-toto", - `
toto default
` - ); - qweb.addTemplate( - "caller", - ` - - - - - - - - - - - - - - - ` - ); - const expected = ` - 4 - - aaa - foo default - - bbb - - - 5 - - aaa - foo default - - bbb - - - 6 - - aaa - foo default - - bbb - - -
toto default
- - `.trim(); - expect(qweb.renderToString("caller").trim()).toBe(expected); - }); -}); - -describe("t-on", () => { - test("can bind event handler", () => { - let a = 1; - const template = ``; - const fragment = renderToDOM(template, { - add() { a = 3} - }); - (fragment.firstChild).click(); - expect(a).toBe(3); - }); - - test("can bind handlers with arguments", () => { - let a = 1; - const template = ``; - const fragment = renderToDOM(template, { - add(n) { a = a + n} - }); - (fragment.firstChild).click(); - expect(a).toBe(6); - }); -}); -