This commit is contained in:
Géry Debongnie
2021-10-11 15:03:56 +02:00
parent 82ab18ad83
commit 1a33d3d8e8
7 changed files with 173 additions and 147 deletions
+61 -41
View File
@@ -44,50 +44,70 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
} else { } else {
exprID = `scope.${value.id}`; exprID = `scope.${value.id}`;
} }
ctx.addIf(`${exprID} != null`);
if (ctx.escaping) { if (ctx.parentTextNode) {
let protectID; ctx.addIf(`${exprID} != null`);
if (value.hasBody) { ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
ctx.rootContext.shouldDefineUtils = true; ctx.closeIf();
protectID = ctx.startProtectScope(); } else if (ctx.parentNode) {
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; ctx.rootContext.shouldDefineUtils = true;
if (value.hasBody) { ctx.addLine(`insertValue(c${ctx.parentNode}, ${exprID})`);
ctx.addLine( } else {
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});` ctx.addIf(`${exprID} != null`);
);
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(); 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({ QWeb.addDirective({
+1
View File
@@ -84,6 +84,7 @@ export class CompilationContext {
this.code.unshift(" let QWeb = this.constructor;"); this.code.unshift(" let QWeb = this.constructor;");
} }
if (this.shouldDefineUtils) { if (this.shouldDefineUtils) {
this.code.unshift(" let insertValue = utils.insertValue;");
this.code.unshift(" let utils = this.constructor.utils;"); this.code.unshift(" let utils = this.constructor.utils;");
} }
return this.code; return this.code;
+17 -2
View File
@@ -1,8 +1,9 @@
import { EventBus } from "../core/event_bus"; import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index"; import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context"; import { CompilationContext } from "./compilation_context";
import { shallowEqual, escape } from "../utils"; import { shallowEqual, escape, _Markup } from "../utils";
import { addNS } from "../vdom/vdom"; import { addNS } from "../vdom/vdom";
import { htmlToVDOM } from "../vdom/html_to_vdom";
/** /**
* Owl QWeb Engine * Owl QWeb Engine
@@ -89,6 +90,16 @@ function isComponent(obj): boolean {
return obj && obj.hasOwnProperty("__owl__"); 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 { class VDomArray extends Array {
toString() { toString() {
return vDomToString(this); return vDomToString(this);
@@ -111,6 +122,7 @@ function vDomToString(vdom: VNode[]): string {
const UTILS: Utils = { const UTILS: Utils = {
zero: Symbol("zero"), zero: Symbol("zero"),
insertValue,
toClassObj(expr) { toClassObj(expr) {
const result = {}; const result = {};
if (typeof expr === "string") { if (typeof expr === "string") {
@@ -599,7 +611,10 @@ export class QWeb extends EventBus {
if (!(dName in QWeb.DIRECTIVE_NAMES)) { if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`); 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( const tNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml", "http://www.w3.org/1999/xhtml",
"t", "t",
+42 -47
View File
@@ -102,68 +102,63 @@ export function shallowEqual(p1, p2): boolean {
return true; return true;
} }
const escapeMethod = Symbol('html')
// notable issues: // notable issues:
// * objects can't be negative in JS, so !!"" -> false but // * objects can't be negative in JS, so !!"" -> false but
// !!(new String) -> true, likewise markup // !!(new String) -> true, likewise markup
// TODO (?) // TODO (?)
// * Markup.join / Markup#join => escapes items and returns a Markup // * Markup.join / Markup#join => escapes items and returns a Markup
// * Markup#replace => automatically escapes the replacements (difficult impl) // * Markup#replace => automatically escapes the replacements (difficult impl)
class _Markup extends String { export class _Markup extends String {}
[escapeMethod]() {
return this;
}
}
/** /**
* Returns a markup object, which acts like a String but is considered safe by * Returns a markup object, which acts like a String but is considered safe by
* `_.escape`, and will therefore be injected as-is (without additional * `_.escape`, and will therefore be injected as-is (without additional
* escaping) in templates. Can be used to inject dynamic HTML in templates * escaping) in templates. Can be used to inject dynamic HTML in templates
* (where the template itself can't), see first example. * (where the template itself can't), see first example.
* *
* Can also be used as a *template tag*, in which case the literal content * 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 * won't be escaped but the substitutions which are not already markup objects
* will be. * will be.
* *
* ## WARNINGS: * ## WARNINGS:
* * A markup object is a `String` (boxed) but not a `string` (primitive), they * * A markup object is a `String` (boxed) but not a `string` (primitive), they
* typecheck differently which can be relevant. * typecheck differently which can be relevant.
* * To strip out the "markupness", just call `String(markup)`. * * To strip out the "markupness", just call `String(markup)`.
* * Most string operations (e.g. concatenation, `String#replace`, ...) will * * Most string operations (e.g. concatenation, `String#replace`, ...) will
* also strip out markupness * also strip out markupness
* * If the input is empty, returns a regular string (that way boolean tests * * If the input is empty, returns a regular string (that way boolean tests
* work as expected). * work as expected).
* *
* @returns a markup object * @returns a markup object
* *
* @example regular function * @example regular function
* let h; * let h;
* if (someTest) { * if (someTest) {
* h = Markup(_t("This is a <strong>success</strong>")); * h = Markup(_t("This is a <strong>success</strong>"));
* } else { * } else {
* h = Markup(_t("Things did <strong>not</strong> work out")); * h = Markup(_t("Things did <strong>not</strong> work out"));
* } * }
* qweb.render("some_template", { message: h }); * qweb.render("some_template", { message: h });
* *
* @example template tag * @example template tag
* const escaped = "<some> text"; * const escaped = "<some> text";
* const asis = Markup`some <b>text</b>`; * const asis = Markup`some <b>text</b>`;
* const h = Markup`Regular strings get ${escaped} but markup is injected ${asis}`; * const h = Markup`Regular strings get ${escaped} but markup is injected ${asis}`;
*/ */
export function Markup(v, ...exprs) { export function Markup(v, ...exprs) {
if (!(v instanceof Array)) { if (!(v instanceof Array)) {
return v ? new _Markup(v) : ''; return v ? new _Markup(v) : "";
} }
const elements = []; const elements = [];
let i = 0; let i = 0;
for(; i < exprs.length; ++i) { for (; i < exprs.length; ++i) {
elements.push(v[i], escape(exprs[i])); elements.push(v[i], escape(exprs[i]));
} }
elements.push(v[i]); elements.push(v[i]);
const s = elements.join(''); const s = elements.join("");
if (!s) { return '' } if (!s) {
return "";
}
return new _Markup(s); return new _Markup(s);
} }
+21 -25
View File
@@ -3064,14 +3064,14 @@ exports[`t-out (formerly t-raw tests) not escaping 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils;
let insertValue = utils.insertValue;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1); let vn1 = h('div', p1, c1);
let _2 = scope['var']; let _2 = scope['var'];
if (_2 != null) { insertValue(c1, _2)
c1.push({text: _2});
}
return vn1; return vn1;
}" }"
`; `;
@@ -3181,14 +3181,14 @@ exports[`t-out escaping 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils;
let insertValue = utils.insertValue;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
let _2 = scope['var']; let _2 = scope['var'];
if (_2 != null) { insertValue(c1, _2)
c1.push({text: _2});
}
return vn1; return vn1;
}" }"
`; `;
@@ -3197,14 +3197,14 @@ exports[`t-out escaping on a node 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils;
let insertValue = utils.insertValue;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
let _2 = 'ok'; let _2 = 'ok';
if (_2 != null) { insertValue(c1, _2)
c1.push({text: _2});
}
return vn1; return vn1;
}" }"
`; `;
@@ -3213,16 +3213,14 @@ exports[`t-out escaping on a node with a body 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils;
let insertValue = utils.insertValue;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
let _2 = 'ok'; let _2 = 'ok';
if (_2 != null) { insertValue(c1, _2)
c1.push({text: _2});
} else {
c1.push({text: \`nope\`});
}
return vn1; return vn1;
}" }"
`; `;
@@ -3231,16 +3229,14 @@ exports[`t-out escaping on a node with a body, as a default 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils;
let insertValue = utils.insertValue;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
let _2 = scope['var']; let _2 = scope['var'];
if (_2 != null) { insertValue(c1, _2)
c1.push({text: _2});
} else {
c1.push({text: \`nope\`});
}
return vn1; return vn1;
}" }"
`; `;
@@ -3249,14 +3245,14 @@ exports[`t-out literal 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils;
let insertValue = utils.insertValue;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
let _2 = 'ok'; let _2 = 'ok';
if (_2 != null) { insertValue(c1, _2)
c1.push({text: _2});
}
return vn1; return vn1;
}" }"
`; `;
@@ -3357,14 +3353,14 @@ exports[`t-out variable 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
// Template name: \\"test\\" // Template name: \\"test\\"
let utils = this.constructor.utils;
let insertValue = utils.insertValue;
let scope = Object.create(context); let scope = Object.create(context);
let h = this.h; let h = this.h;
let c1 = [], p1 = {key:1}; let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1); let vn1 = h('span', p1, c1);
let _2 = scope['var']; let _2 = scope['var'];
if (_2 != null) { insertValue(c1, _2)
c1.push({text: _2});
}
return vn1; return vn1;
}" }"
`; `;
+2 -2
View File
@@ -150,7 +150,7 @@ describe("error handling", () => {
}); });
}); });
describe("t-out", () => { describe.only("t-out", () => {
test("literal", () => { test("literal", () => {
qweb.addTemplate("test", `<span><t t-out="'ok'"/></span>`); qweb.addTemplate("test", `<span><t t-out="'ok'"/></span>`);
expect(renderToString(qweb, "test")).toBe("<span>ok</span>"); expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
@@ -178,7 +178,7 @@ describe("t-out", () => {
expect(renderToString(qweb, "test")).toBe("<span>ok</span>"); expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
}); });
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", `<span t-out="var">nope</span>`); qweb.addTemplate("test", `<span t-out="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>"); expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
}); });
+29 -30
View File
@@ -32,36 +32,35 @@ describe("old t-esc directive", () => {
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>" "<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
); );
}); });
}); });
describe("old t-raw directive", () => { describe("old t-raw directive", () => {
test("literal", () => { test("literal", () => {
qweb.addTemplate("test", `<span><t t-raw="'ok'"/></span>`); qweb.addTemplate("test", `<span><t t-raw="'ok'"/></span>`);
expect(renderToString(qweb, "test")).toBe("<span>ok</span>"); expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
}); });
test("variable", () => { test("variable", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`); qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>"); expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
}); });
test("not escaping", () => { test("not escaping", () => {
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`); qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>"); expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
}); });
test("t-raw and another sibling node", () => { test("t-raw and another sibling node", () => {
qweb.addTemplate("test", `<span><span>hello</span><t t-raw="var"/></span>`); qweb.addTemplate("test", `<span><span>hello</span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>world</ok>" })).toBe( expect(renderToString(qweb, "test", { var: "<ok>world</ok>" })).toBe(
"<span><span>hello</span><ok>world</ok></span>" "<span><span>hello</span><ok>world</ok></span>"
); );
}); });
test("t-raw with comment", () => { test("t-raw with comment", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`); qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<p>text<!-- top secret --></p>" })).toBe( expect(renderToString(qweb, "test", { var: "<p>text<!-- top secret --></p>" })).toBe(
"<span><p>text<!-- top secret --></p></span>" "<span><p>text<!-- top secret --></p></span>"
); );
}); });
}); });