From 1a33d3d8e83d37365b84ccba8766d6a6ed67ecb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 11 Oct 2021 15:03:56 +0200 Subject: [PATCH] wip --- src/qweb/base_directives.ts | 102 ++++++++++++--------- src/qweb/compilation_context.ts | 1 + src/qweb/qweb.ts | 19 +++- src/utils.ts | 89 +++++++++--------- tests/qweb/__snapshots__/qweb.test.ts.snap | 46 +++++----- tests/qweb/qweb.test.ts | 4 +- tests/qweb/qweb_t_esc_t_raw.test.ts | 59 ++++++------ 7 files changed, 173 insertions(+), 147 deletions(-) diff --git a/src/qweb/base_directives.ts b/src/qweb/base_directives.ts index 0684bd29..da9dbe5a 100644 --- a/src/qweb/base_directives.ts +++ b/src/qweb/base_directives.ts @@ -44,50 +44,70 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio } else { exprID = `scope.${value.id}`; } - ctx.addIf(`${exprID} != null`); - if (ctx.escaping) { - let protectID; - if (value.hasBody) { - ctx.rootContext.shouldDefineUtils = true; - protectID = ctx.startProtectScope(); - ctx.addLine( - `${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};` - ); - } - if (ctx.parentTextNode) { - ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`); - } else if (ctx.parentNode) { - ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`); - } else { - let nodeID = ctx.generateID(); - ctx.rootContext.rootNode = nodeID; - ctx.rootContext.parentTextNode = nodeID; - ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`); - if (ctx.rootContext.shouldDefineResult) { - ctx.addLine(`result = vn${nodeID}`); - } - } - if (value.hasBody) { - ctx.stopProtectScope(protectID); - } - } else { + if (ctx.parentTextNode) { + ctx.addIf(`${exprID} != null`); + ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`); + ctx.closeIf(); + } else if (ctx.parentNode) { ctx.rootContext.shouldDefineUtils = true; - if (value.hasBody) { - ctx.addLine( - `const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});` - ); - ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`); - } else { - ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`); - } - } - if (node.childNodes.length) { - ctx.addElse(); - qweb._compileChildren(node, ctx); - } + ctx.addLine(`insertValue(c${ctx.parentNode}, ${exprID})`); + } else { + ctx.addIf(`${exprID} != null`); - ctx.closeIf(); + let nodeID = ctx.generateID(); + ctx.rootContext.rootNode = nodeID; + ctx.rootContext.parentTextNode = nodeID; + ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`); + if (ctx.rootContext.shouldDefineResult) { + ctx.addLine(`result = vn${nodeID}`); + } + ctx.closeIf(); + } + // ctx.addIf(`${exprID} != null`); + + // if (ctx.escaping) { + // let protectID; + // if (value.hasBody) { + // ctx.rootContext.shouldDefineUtils = true; + // protectID = ctx.startProtectScope(); + // ctx.addLine( + // `${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};` + // ); + // } + // if (ctx.parentTextNode) { + // ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`); + // } else if (ctx.parentNode) { + // ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`); + // } else { + // let nodeID = ctx.generateID(); + // ctx.rootContext.rootNode = nodeID; + // ctx.rootContext.parentTextNode = nodeID; + // ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`); + // if (ctx.rootContext.shouldDefineResult) { + // ctx.addLine(`result = vn${nodeID}`); + // } + // } + // if (value.hasBody) { + // ctx.stopProtectScope(protectID); + // } + // } else { + // ctx.rootContext.shouldDefineUtils = true; + // if (value.hasBody) { + // ctx.addLine( + // `const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});` + // ); + // ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`); + // } else { + // ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`); + // } + // } + // if (node.childNodes.length) { + // ctx.addElse(); + // qweb._compileChildren(node, ctx); + // } + + // ctx.closeIf(); } QWeb.addDirective({ diff --git a/src/qweb/compilation_context.ts b/src/qweb/compilation_context.ts index 38f0c46b..f321c758 100644 --- a/src/qweb/compilation_context.ts +++ b/src/qweb/compilation_context.ts @@ -84,6 +84,7 @@ export class CompilationContext { this.code.unshift(" let QWeb = this.constructor;"); } if (this.shouldDefineUtils) { + this.code.unshift(" let insertValue = utils.insertValue;"); this.code.unshift(" let utils = this.constructor.utils;"); } return this.code; diff --git a/src/qweb/qweb.ts b/src/qweb/qweb.ts index f01e2691..065c5e48 100644 --- a/src/qweb/qweb.ts +++ b/src/qweb/qweb.ts @@ -1,8 +1,9 @@ import { EventBus } from "../core/event_bus"; import { h, patch, VNode } from "../vdom/index"; import { CompilationContext } from "./compilation_context"; -import { shallowEqual, escape } from "../utils"; +import { shallowEqual, escape, _Markup } from "../utils"; import { addNS } from "../vdom/vdom"; +import { htmlToVDOM } from "../vdom/html_to_vdom"; /** * Owl QWeb Engine @@ -89,6 +90,16 @@ function isComponent(obj): boolean { return obj && obj.hasOwnProperty("__owl__"); } +function insertValue(children: any[], value: any) { + if (value != null) { + if (value instanceof _Markup) { + children.push(...htmlToVDOM(value as any)); + } else { + children.push({ text: value }); + } + } +} + class VDomArray extends Array { toString() { return vDomToString(this); @@ -111,6 +122,7 @@ function vDomToString(vdom: VNode[]): string { const UTILS: Utils = { zero: Symbol("zero"), + insertValue, toClassObj(expr) { const result = {}; if (typeof expr === "string") { @@ -599,7 +611,10 @@ export class QWeb extends EventBus { if (!(dName in QWeb.DIRECTIVE_NAMES)) { throw new Error(`Unknown QWeb directive: '${attrName}'`); } - if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-out" || attrName === "t-raw")) { + if ( + node.tagName !== "t" && + (attrName === "t-esc" || attrName === "t-out" || attrName === "t-raw") + ) { const tNode = document.implementation.createDocument( "http://www.w3.org/1999/xhtml", "t", diff --git a/src/utils.ts b/src/utils.ts index 3f222623..7c846bd6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -102,68 +102,63 @@ export function shallowEqual(p1, p2): boolean { return true; } -const escapeMethod = Symbol('html') - // notable issues: // * objects can't be negative in JS, so !!"" -> false but // !!(new String) -> true, likewise markup // TODO (?) // * Markup.join / Markup#join => escapes items and returns a Markup // * Markup#replace => automatically escapes the replacements (difficult impl) -class _Markup extends String { - [escapeMethod]() { - return this; - } -} +export class _Markup extends String {} /** -* Returns a markup object, which acts like a String but is considered safe by -* `_.escape`, and will therefore be injected as-is (without additional -* escaping) in templates. Can be used to inject dynamic HTML in templates -* (where the template itself can't), see first example. -* -* Can also be used as a *template tag*, in which case the literal content -* won't be escaped but the substitutions which are not already markup objects -* will be. -* -* ## WARNINGS: -* * A markup object is a `String` (boxed) but not a `string` (primitive), they -* typecheck differently which can be relevant. -* * To strip out the "markupness", just call `String(markup)`. -* * Most string operations (e.g. concatenation, `String#replace`, ...) will -* also strip out markupness -* * If the input is empty, returns a regular string (that way boolean tests -* work as expected). -* -* @returns a markup object -* -* @example regular function -* let h; -* if (someTest) { -* h = Markup(_t("This is a success")); -* } else { -* h = Markup(_t("Things did not work out")); -* } -* qweb.render("some_template", { message: h }); -* -* @example template tag -* const escaped = " text"; -* const asis = Markup`some text`; -* const h = Markup`Regular strings get ${escaped} but markup is injected ${asis}`; -*/ + * Returns a markup object, which acts like a String but is considered safe by + * `_.escape`, and will therefore be injected as-is (without additional + * escaping) in templates. Can be used to inject dynamic HTML in templates + * (where the template itself can't), see first example. + * + * Can also be used as a *template tag*, in which case the literal content + * won't be escaped but the substitutions which are not already markup objects + * will be. + * + * ## WARNINGS: + * * A markup object is a `String` (boxed) but not a `string` (primitive), they + * typecheck differently which can be relevant. + * * To strip out the "markupness", just call `String(markup)`. + * * Most string operations (e.g. concatenation, `String#replace`, ...) will + * also strip out markupness + * * If the input is empty, returns a regular string (that way boolean tests + * work as expected). + * + * @returns a markup object + * + * @example regular function + * let h; + * if (someTest) { + * h = Markup(_t("This is a success")); + * } else { + * h = Markup(_t("Things did not work out")); + * } + * qweb.render("some_template", { message: h }); + * + * @example template tag + * const escaped = " text"; + * const asis = Markup`some text`; + * const h = Markup`Regular strings get ${escaped} but markup is injected ${asis}`; + */ export function Markup(v, ...exprs) { if (!(v instanceof Array)) { - return v ? new _Markup(v) : ''; + return v ? new _Markup(v) : ""; } const elements = []; let i = 0; - for(; i < exprs.length; ++i) { - elements.push(v[i], escape(exprs[i])); + for (; i < exprs.length; ++i) { + elements.push(v[i], escape(exprs[i])); } elements.push(v[i]); - const s = elements.join(''); - if (!s) { return '' } + const s = elements.join(""); + if (!s) { + return ""; + } return new _Markup(s); } - diff --git a/tests/qweb/__snapshots__/qweb.test.ts.snap b/tests/qweb/__snapshots__/qweb.test.ts.snap index 5ebaa6ce..9c258883 100644 --- a/tests/qweb/__snapshots__/qweb.test.ts.snap +++ b/tests/qweb/__snapshots__/qweb.test.ts.snap @@ -3064,14 +3064,14 @@ exports[`t-out (formerly t-raw tests) not escaping 1`] = ` "function anonymous(context, extra ) { // Template name: \\"test\\" + let utils = this.constructor.utils; + let insertValue = utils.insertValue; let scope = Object.create(context); let h = this.h; let c1 = [], p1 = {key:1}; let vn1 = h('div', p1, c1); let _2 = scope['var']; - if (_2 != null) { - c1.push({text: _2}); - } + insertValue(c1, _2) return vn1; }" `; @@ -3181,14 +3181,14 @@ exports[`t-out escaping 1`] = ` "function anonymous(context, extra ) { // Template name: \\"test\\" + let utils = this.constructor.utils; + let insertValue = utils.insertValue; let scope = Object.create(context); let h = this.h; let c1 = [], p1 = {key:1}; let vn1 = h('span', p1, c1); let _2 = scope['var']; - if (_2 != null) { - c1.push({text: _2}); - } + insertValue(c1, _2) return vn1; }" `; @@ -3197,14 +3197,14 @@ exports[`t-out escaping on a node 1`] = ` "function anonymous(context, extra ) { // Template name: \\"test\\" + let utils = this.constructor.utils; + let insertValue = utils.insertValue; let scope = Object.create(context); let h = this.h; let c1 = [], p1 = {key:1}; let vn1 = h('span', p1, c1); let _2 = 'ok'; - if (_2 != null) { - c1.push({text: _2}); - } + insertValue(c1, _2) return vn1; }" `; @@ -3213,16 +3213,14 @@ exports[`t-out escaping on a node with a body 1`] = ` "function anonymous(context, extra ) { // Template name: \\"test\\" + let utils = this.constructor.utils; + let insertValue = utils.insertValue; let scope = Object.create(context); let h = this.h; let c1 = [], p1 = {key:1}; let vn1 = h('span', p1, c1); let _2 = 'ok'; - if (_2 != null) { - c1.push({text: _2}); - } else { - c1.push({text: \`nope\`}); - } + insertValue(c1, _2) return vn1; }" `; @@ -3231,16 +3229,14 @@ exports[`t-out escaping on a node with a body, as a default 1`] = ` "function anonymous(context, extra ) { // Template name: \\"test\\" + let utils = this.constructor.utils; + let insertValue = utils.insertValue; let scope = Object.create(context); let h = this.h; let c1 = [], p1 = {key:1}; let vn1 = h('span', p1, c1); let _2 = scope['var']; - if (_2 != null) { - c1.push({text: _2}); - } else { - c1.push({text: \`nope\`}); - } + insertValue(c1, _2) return vn1; }" `; @@ -3249,14 +3245,14 @@ exports[`t-out literal 1`] = ` "function anonymous(context, extra ) { // Template name: \\"test\\" + let utils = this.constructor.utils; + let insertValue = utils.insertValue; let scope = Object.create(context); let h = this.h; let c1 = [], p1 = {key:1}; let vn1 = h('span', p1, c1); let _2 = 'ok'; - if (_2 != null) { - c1.push({text: _2}); - } + insertValue(c1, _2) return vn1; }" `; @@ -3357,14 +3353,14 @@ exports[`t-out variable 1`] = ` "function anonymous(context, extra ) { // Template name: \\"test\\" + let utils = this.constructor.utils; + let insertValue = utils.insertValue; let scope = Object.create(context); let h = this.h; let c1 = [], p1 = {key:1}; let vn1 = h('span', p1, c1); let _2 = scope['var']; - if (_2 != null) { - c1.push({text: _2}); - } + insertValue(c1, _2) return vn1; }" `; diff --git a/tests/qweb/qweb.test.ts b/tests/qweb/qweb.test.ts index 8cc5b5f5..d04a089a 100644 --- a/tests/qweb/qweb.test.ts +++ b/tests/qweb/qweb.test.ts @@ -150,7 +150,7 @@ describe("error handling", () => { }); }); -describe("t-out", () => { +describe.only("t-out", () => { test("literal", () => { qweb.addTemplate("test", ``); expect(renderToString(qweb, "test")).toBe("ok"); @@ -178,7 +178,7 @@ describe("t-out", () => { expect(renderToString(qweb, "test")).toBe("ok"); }); - test("escaping on a node with a body, as a default", () => { + test.only("escaping on a node with a body, as a default", () => { qweb.addTemplate("test", `nope`); expect(renderToString(qweb, "test")).toBe("nope"); }); diff --git a/tests/qweb/qweb_t_esc_t_raw.test.ts b/tests/qweb/qweb_t_esc_t_raw.test.ts index b770511c..ff273a7f 100644 --- a/tests/qweb/qweb_t_esc_t_raw.test.ts +++ b/tests/qweb/qweb_t_esc_t_raw.test.ts @@ -32,36 +32,35 @@ describe("old t-esc directive", () => { "&lt;ok&gt;abc&lt;/ok&gt;" ); }); - }); describe("old t-raw directive", () => { - test("literal", () => { - qweb.addTemplate("test", ``); - expect(renderToString(qweb, "test")).toBe("ok"); - }); - - test("variable", () => { - qweb.addTemplate("test", ``); - expect(renderToString(qweb, "test", { var: "ok" })).toBe("ok"); - }); - - test("not escaping", () => { - qweb.addTemplate("test", `
`); - expect(renderToString(qweb, "test", { var: "" })).toBe("
"); - }); - - test("t-raw and another sibling node", () => { - qweb.addTemplate("test", `hello`); - expect(renderToString(qweb, "test", { var: "world" })).toBe( - "helloworld" - ); - }); - - test("t-raw with comment", () => { - qweb.addTemplate("test", ``); - expect(renderToString(qweb, "test", { var: "

text

" })).toBe( - "

text

" - ); - }); - }); \ No newline at end of file + test("literal", () => { + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test")).toBe("ok"); + }); + + test("variable", () => { + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test", { var: "ok" })).toBe("ok"); + }); + + test("not escaping", () => { + qweb.addTemplate("test", `
`); + expect(renderToString(qweb, "test", { var: "" })).toBe("
"); + }); + + test("t-raw and another sibling node", () => { + qweb.addTemplate("test", `hello`); + expect(renderToString(qweb, "test", { var: "world" })).toBe( + "helloworld" + ); + }); + + test("t-raw with comment", () => { + qweb.addTemplate("test", ``); + expect(renderToString(qweb, "test", { var: "

text

" })).toBe( + "

text

" + ); + }); +});