diff --git a/src/blockdom/block_compiler.ts b/src/blockdom/block_compiler.ts
index dda42947..728fe6b5 100644
--- a/src/blockdom/block_compiler.ts
+++ b/src/blockdom/block_compiler.ts
@@ -116,6 +116,7 @@ interface IntermediateTree {
forceRef?: boolean;
refIdx?: number;
refN: number;
+ currentNS: string | null;
}
function buildTree(
@@ -124,9 +125,10 @@ function buildTree(
domParentTree: IntermediateTree | null = null
): IntermediateTree {
switch (node.nodeType) {
- case 1: {
+ case Node.ELEMENT_NODE: {
// HTMLElement
let isActive = false;
+ let currentNS = parent && parent.currentNS;
const tagName = (node as Element).tagName;
let el: Node | undefined = undefined;
const info: DynamicInfo[] = [];
@@ -143,11 +145,18 @@ function buildTree(
el = document.createTextNode("");
isActive = true;
}
- if (!el) {
- el = document.createElement(tagName);
+ const attrs = (node as Element).attributes;
+ const ns = attrs.getNamedItem("block-ns");
+ if (ns) {
+ attrs.removeNamedItem("block-ns");
+ currentNS = ns.value;
}
- if (el instanceof HTMLElement) {
- const attrs = (node as Element).attributes;
+ if (!el) {
+ el = currentNS
+ ? document.createElementNS(currentNS, tagName)
+ : document.createElement(tagName);
+ }
+ if (el instanceof Element) {
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name;
const attrValue = attrs[i].value;
@@ -193,13 +202,14 @@ function buildTree(
el,
info,
refN: isActive ? 1 : 0,
+ currentNS,
};
if (node.firstChild) {
const childNode = node.childNodes[0];
if (
node.childNodes.length === 1 &&
- childNode.nodeType === 1 &&
+ childNode.nodeType === Node.ELEMENT_NODE &&
(childNode as Element).tagName.startsWith("block-child-")
) {
const tagName = (childNode as Element).tagName;
@@ -227,11 +237,11 @@ function buildTree(
}
return tree;
}
- case 3:
- case 8: {
+ case Node.TEXT_NODE:
+ case Node.COMMENT_NODE: {
// text node or comment node
const el =
- node.nodeType === 3
+ node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.textContent!)
: document.createComment(node.textContent!);
return {
@@ -241,6 +251,7 @@ function buildTree(
el,
info: [],
refN: 0,
+ currentNS: null,
};
}
}
diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts
index fdf540d1..11251e0b 100644
--- a/src/compiler/code_generator.ts
+++ b/src/compiler/code_generator.ts
@@ -497,6 +497,10 @@ export class CodeGenerator {
}
// attributes
const attrs: { [key: string]: string } = {};
+ if (ast.ns) {
+ // specific namespace uri
+ attrs["block-ns"] = ast.ns;
+ }
for (let key in ast.attrs) {
if (key.startsWith("t-attf")) {
let expr = interpolate(ast.attrs[key]);
diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts
index a70bb2f4..93bd8c17 100644
--- a/src/compiler/parser.ts
+++ b/src/compiler/parser.ts
@@ -49,6 +49,7 @@ export interface ASTDomNode {
shouldTrim: boolean;
shouldNumberize: boolean;
} | null;
+ ns: string | null;
}
export interface ASTMulti {
@@ -171,11 +172,12 @@ export type AST =
// -----------------------------------------------------------------------------
interface ParsingContext {
inPreTag: boolean;
+ inSVG: boolean;
}
export function parse(xml: string | Node): AST {
const node = xml instanceof Element ? xml : parseXML(`${xml}`).firstChild!;
- const ctx = { inPreTag: false };
+ const ctx = { inPreTag: false, inSVG: false };
const ast = parseNode(node, ctx);
if (!ast) {
return { type: ASTType.Text, value: "" };
@@ -240,7 +242,7 @@ const lineBreakRE = /[\r\n]/;
const whitespaceRE = /\s+/g;
function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null {
- if (node.nodeType === 3) {
+ if (node.nodeType === Node.TEXT_NODE) {
let value = node.textContent || "";
if (!ctx.inPreTag) {
if (lineBreakRE.test(value) && !value.trim()) {
@@ -250,7 +252,7 @@ function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null
}
return { type: ASTType.Text, value };
- } else if (node.nodeType === 8) {
+ } else if (node.nodeType === Node.COMMENT_NODE) {
return { type: ASTType.Comment, value: node.textContent || "" };
}
return null;
@@ -289,18 +291,18 @@ const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const { tagName } = node;
- let dynamicTag = null;
- if (node.hasAttribute("t-tag")) {
- dynamicTag = node.getAttribute("t-tag");
- node.removeAttribute("t-tag");
- }
+ const dynamicTag = node.getAttribute("t-tag");
+ node.removeAttribute("t-tag");
if (tagName === "t" && !dynamicTag) {
return null;
}
const children: AST[] = [];
if (tagName === "pre") {
- ctx = { inPreTag: true };
+ ctx.inPreTag = true;
}
+ const shouldAddSVGNS = tagName === "svg" || (tagName === "g" && !ctx.inSVG);
+ ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
+ const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
@@ -381,6 +383,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
ref,
content: children,
model,
+ ns,
};
}
diff --git a/tests/blockdom/namespace.test.ts b/tests/blockdom/namespace.test.ts
new file mode 100644
index 00000000..4d2a7d4f
--- /dev/null
+++ b/tests/blockdom/namespace.test.ts
@@ -0,0 +1,72 @@
+import { createBlock, mount } from "../../src/blockdom";
+import { makeTestFixture } from "./helpers";
+
+//------------------------------------------------------------------------------
+// Setup and helpers
+//------------------------------------------------------------------------------
+
+const XHTML_URI = "http://www.w3.org/1999/xhtml";
+const SVG_URI = "http://www.w3.org/2000/svg";
+let fixture: HTMLElement;
+
+beforeEach(() => {
+ fixture = makeTestFixture();
+});
+
+afterEach(() => {
+ fixture.remove();
+});
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe("namespace", () => {
+ test("default namespace is xhtml", () => {
+ const block = createBlock(``);
+ const tree = block();
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+ expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI);
+ });
+
+ test("namespace can be changed with block-ns", () => {
+ const block = createBlock(``);
+ const tree = block();
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+ expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI);
+ });
+
+ test("namespace is kept for children", () => {
+ const block = createBlock(
+ ``
+ );
+ const tree = block();
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe(
+ ""
+ );
+ const parent = fixture.firstElementChild!;
+ const child1 = parent.firstElementChild!;
+ const subchild = child1.firstElementChild!;
+ const child2 = child1.nextElementSibling!;
+ expect(parent.namespaceURI).toBe(SVG_URI);
+ expect(child1.namespaceURI).toBe(SVG_URI);
+ expect(child2.namespaceURI).toBe(SVG_URI);
+ expect(subchild.namespaceURI).toBe(SVG_URI);
+ });
+
+ test("various namespaces in same block", () => {
+ const block = createBlock(``);
+ const tree = block();
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+ const none = fixture.firstElementChild!;
+ const one = none.firstElementChild!;
+ const two = one.nextElementSibling!;
+ expect(none.namespaceURI).toBe(XHTML_URI);
+ expect(one.namespaceURI).toBe("one");
+ expect(two.namespaceURI).toBe("two");
+ });
+});
diff --git a/tests/compiler/__snapshots__/svg.test.ts.snap b/tests/compiler/__snapshots__/svg.test.ts.snap
new file mode 100644
index 00000000..cfecd40f
--- /dev/null
+++ b/tests/compiler/__snapshots__/svg.test.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`properly support svg add proper namespace to g tags 1`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
+
+ let block1 = createBlock(\` \`);
+
+ return function template(ctx, node, key = \\"\\") {
+ return block1();
+ }
+}"
+`;
+
+exports[`properly support svg add proper namespace to svg 1`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
+
+ let block1 = createBlock(\`\`);
+
+ return function template(ctx, node, key = \\"\\") {
+ return block1();
+ }
+}"
+`;
diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts
index e32f805e..e2cee68a 100644
--- a/tests/compiler/parser.test.ts
+++ b/tests/compiler/parser.test.ts
@@ -46,6 +46,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
});
});
@@ -73,6 +74,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
content: [],
+ ns: null,
});
});
@@ -86,6 +88,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
content: [{ type: ASTType.Text, value: "some text" }],
+ ns: null,
});
});
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [
{ type: ASTType.Text, value: "some text" },
{
@@ -108,6 +112,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "inside" }],
},
],
@@ -126,6 +131,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [],
},
{
@@ -136,6 +142,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [],
},
],
@@ -155,6 +162,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [],
},
],
@@ -170,10 +178,83 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "foo" }],
});
});
+ test("svg dom node", async () => {
+ expect(
+ parse(
+ ``
+ )
+ ).toEqual({
+ attrs: {
+ height: "90px",
+ width: "100px",
+ },
+ content: [
+ {
+ attrs: {
+ cx: "50",
+ cy: "50",
+ fill: "yellow",
+ r: "4",
+ stroke: "green",
+ "stroke-width": "1",
+ },
+ content: [],
+ dynamicTag: null,
+ model: null,
+ ns: null,
+ on: {},
+ ref: null,
+ tag: "circle",
+ type: 2,
+ },
+ ],
+ dynamicTag: null,
+ model: null,
+ ns: "http://www.w3.org/2000/svg",
+ on: {},
+ ref: null,
+ tag: "svg",
+ type: 2,
+ });
+ expect(
+ parse(``)
+ ).toEqual({
+ attrs: {},
+ content: [
+ {
+ attrs: {
+ cx: "50",
+ cy: "50",
+ fill: "yellow",
+ r: "4",
+ stroke: "green",
+ "stroke-width": "1",
+ },
+ content: [],
+ dynamicTag: null,
+ model: null,
+ ns: null,
+ on: {},
+ ref: null,
+ tag: "circle",
+ type: 2,
+ },
+ ],
+ dynamicTag: null,
+ model: null,
+ ns: "http://www.w3.org/2000/svg",
+ on: {},
+ ref: null,
+ tag: "g",
+ type: 2,
+ });
+ });
+
// ---------------------------------------------------------------------------
// t-esc
// ---------------------------------------------------------------------------
@@ -200,6 +281,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "" }],
});
});
@@ -221,6 +303,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "hey" }],
});
});
@@ -262,6 +345,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.TOut, expr: "text", body: null }],
});
});
@@ -275,6 +359,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [
{ type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] },
],
@@ -294,6 +379,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [
{
type: ASTType.TIf,
@@ -321,6 +407,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
},
tElif: null,
@@ -397,6 +484,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [
{
type: ASTType.Text,
@@ -415,6 +503,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "elif" }],
},
},
@@ -427,6 +516,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [
{
type: ASTType.Text,
@@ -474,6 +564,7 @@ describe("qweb parser", () => {
dynamicTag: null,
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "ok" }],
},
],
@@ -493,6 +584,7 @@ describe("qweb parser", () => {
dynamicTag: null,
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "ok" }],
},
{ type: ASTType.Text, value: "abc" },
@@ -527,6 +619,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [
{
type: ASTType.TIf,
@@ -601,6 +694,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
memo: "",
@@ -646,6 +740,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
memo: "",
@@ -681,6 +776,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
},
@@ -715,6 +811,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "category.name", defaultValue: "" }],
},
memo: "",
@@ -736,6 +833,7 @@ describe("qweb parser", () => {
model: null,
tag: "div",
dynamicTag: null,
+ ns: null,
content: [
{
type: ASTType.TForEach,
@@ -781,6 +879,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
attrs: {},
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
},
memo: "",
@@ -888,6 +987,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [
{
type: ASTType.TCall,
@@ -925,6 +1025,7 @@ describe("qweb parser", () => {
on: { click: "add" },
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "Click" }],
});
});
@@ -1003,6 +1104,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
on: {},
+ ns: null,
},
{
type: ASTType.DomNode,
@@ -1013,6 +1115,7 @@ describe("qweb parser", () => {
ref: null,
model: null,
on: {},
+ ns: null,
},
],
},
@@ -1203,6 +1306,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
},
});
@@ -1220,6 +1324,7 @@ describe("qweb parser", () => {
on: {},
ref: null,
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
},
});
@@ -1238,6 +1343,7 @@ describe("qweb parser", () => {
on: {},
ref: "name",
model: null,
+ ns: null,
content: [{ type: ASTType.Text, value: "hey" }],
});
});
@@ -1251,6 +1357,7 @@ describe("qweb parser", () => {
on: {},
ref: "name",
model: null,
+ ns: null,
content: [
{ type: ASTType.TOut, expr: "text", body: [{ type: ASTType.Text, value: "body" }] },
],
@@ -1266,6 +1373,7 @@ describe("qweb parser", () => {
on: {},
ref: "name",
model: null,
+ ns: null,
content: [{ type: ASTType.TEsc, expr: "text", defaultValue: "body" }],
});
});
@@ -1309,7 +1417,8 @@ describe("qweb parser", () => {
model: null,
tag: "div",
dynamicTag: null,
- type: 2,
+ type: ASTType.DomNode,
+ ns: null,
},
type: 16,
},
@@ -1339,6 +1448,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
+ ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1357,6 +1467,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
+ ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1375,6 +1486,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
+ ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1394,6 +1506,7 @@ describe("qweb parser", () => {
ref: null,
tag: "textarea",
dynamicTag: null,
+ ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1412,6 +1525,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
+ ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1430,6 +1544,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
+ ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1448,6 +1563,7 @@ describe("qweb parser", () => {
ref: null,
tag: "input",
dynamicTag: null,
+ ns: null,
model: {
baseExpr: "state",
expr: "'stuff'",
@@ -1472,6 +1588,7 @@ describe("qweb parser", () => {
tag: "div",
dynamicTag: "theTag",
model: null,
+ ns: null,
});
});
});
diff --git a/tests/compiler/svg.test.ts b/tests/compiler/svg.test.ts
index a3113efe..ff450a08 100644
--- a/tests/compiler/svg.test.ts
+++ b/tests/compiler/svg.test.ts
@@ -1,22 +1,20 @@
-describe("properly support svg", () => {
- test.skip("add proper namespace to svg", () => {
- // qweb.addTemplate(
- // "test",
- // ``
- // );
- // expect(renderToString(qweb, "test")).toBe(
- // ``
- // );
+import { renderToString, snapshotEverything } from "../helpers";
+
+snapshotEverything();
+
+describe.only("properly support svg", () => {
+ test("add proper namespace to svg", () => {
+ const template = ``;
+ expect(renderToString(template)).toBe(
+ ``
+ );
});
- test.skip("add proper namespace to g tags", () => {
+ test("add proper namespace to g tags", () => {
// this is necessary if one wants to use components in a svg
- // qweb.addTemplate(
- // "test",
- // ` `
- // );
- // expect(renderToString(qweb, "test")).toBe(
- // ` `
- // );
+ const template = ` `;
+ expect(renderToString(template)).toBe(
+ ` `
+ );
});
});
diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap
index 94ce15bf..41d5ff4b 100644
--- a/tests/components/__snapshots__/basics.test.ts.snap
+++ b/tests/components/__snapshots__/basics.test.ts.snap
@@ -1192,7 +1192,7 @@ exports[`support svg components add proper namespace to svg 1`] = `
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
- let block1 = createBlock(\`\`);
+ let block1 = createBlock(\`\`);
return function template(ctx, node, key = \\"\\") {
return block1();
@@ -1206,7 +1206,7 @@ exports[`support svg components add proper namespace to svg 2`] = `
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
- let block1 = createBlock(\`\`);
+ let block1 = createBlock(\`\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`GComp\`, {}, key + \`__1\`, node, ctx);