Skip to content

Commit

Permalink
[FIX] compiler: fix xmlns attribute not being set correctly in firefox
Browse files Browse the repository at this point in the history
Firefox will not serialize the xmlns attributes of node inside
XMLDocuments, see https://bugzilla.mozilla.org/show_bug.cgi?id=175946
it will only output an xmlns attribute if the node being serialized has
a non-null namespaceURI.

When compiling templates, we currently use a special attribute,
"block-ns" to keep track of the namespace, but we do not make use of the
namespace to create the elements that will be serialized to the compiled
block string. This causes a difference in behaviour between Chrome and
Firefox: since we end up with an Element with two attributes: the
block-ns attribute and the xmlns attribute. Chrome will serialize both,
but Firefox will only serialize the block-ns because the Element does
not have a namespaceURI.

To fix this issue, we get rid of the block-ns magic attribute
completely, and use xmlns instead. We also use the namespace when
creating intermediate elements that will be used to create the
serialized block string: the namespace is only used to create the
elements but *not* set as an attribute, as this would cause chrome to
serialize it twice, causing a duplicate attribute error when parsing it
further down the line.

When creating the template element for the block at runtime, the xmlns
attribute is used both as the namespace with which to create the
element, and set as an attribute, this doesn't cause issues when
serializing later because the namespaceURI is never serialized as an
attribute when serializing an HTML document[1], so we avoid the
double-serialization in Chrome, and when doing HTML serialization,
Firefox will correctly serialize the attribute.

It's desirable that the xmlns is set as an attribute to allow users to
use owl to render SVG, and then use the HTML serialization of it as a
SVG even outside the context of an HTML document (eg to generate an SVG
file or an SVG data URL).

[1]: https://w3c.github.io/DOM-Parsing/#xml-serialization
  • Loading branch information
sdegueldre committed Aug 7, 2023
1 parent 105ec7c commit 7108296
Show file tree
Hide file tree
Showing 9 changed files with 50 additions and 45 deletions.
10 changes: 4 additions & 6 deletions src/compiler/code_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,11 +586,6 @@ export class CodeGenerator {
}
// attributes
const attrs: Attrs = {};
const nameSpace = ast.ns || ctx.nameSpace;
if (nameSpace && isNewBlock) {
// specific namespace uri
attrs["block-ns"] = nameSpace;
}

for (let key in ast.attrs) {
let expr, attrName;
Expand Down Expand Up @@ -719,7 +714,10 @@ export class CodeGenerator {
attrs["block-ref"] = String(idx);
}

const dom = xmlDoc.createElement(ast.tag);
const nameSpace = ast.ns || ctx.nameSpace;
const dom = nameSpace
? xmlDoc.createElementNS(nameSpace, ast.tag)
: xmlDoc.createElement(ast.tag);
for (const [attr, val] of Object.entries(attrs)) {
if (!(attr === "class" && val === "")) {
dom.setAttribute(attr, val);
Expand Down
14 changes: 9 additions & 5 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,14 @@ export function parse(xml: string | Element): AST {

function _parse(xml: Element): AST {
normalizeXML(xml);
const ctx = { inPreTag: false, inSVG: false };
const ctx = { inPreTag: false };
return parseNode(xml, ctx) || { type: ASTType.Text, value: "" };
}

interface ParsingContext {
tModelInfo?: TModelInfo | null;
nameSpace?: string;
inPreTag: boolean;
inSVG: boolean;
}

function parseNode(node: Node, ctx: ParsingContext): AST | null {
Expand Down Expand Up @@ -323,9 +323,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
if (tagName === "pre") {
ctx.inPreTag = true;
}
const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG;
ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;

let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? "http://www.w3.org/2000/svg" : null;
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");

Expand Down Expand Up @@ -389,6 +388,8 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
}
} else if (attr.startsWith("block-")) {
throw new OwlError(`Invalid attribute: '${attr}'`);
} else if (attr === "xmlns") {
ns = value;
} else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
Expand All @@ -401,6 +402,9 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
attrs[attr] = value;
}
}
if (ns) {
ctx.nameSpace = ns;
}

const children = parseChildren(node, ctx);
return {
Expand Down
8 changes: 2 additions & 6 deletions src/runtime/blockdom/block_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,7 @@ function buildTree(
info.push({ type: "child", idx: index });
el = document.createTextNode("");
}
const attrs = (node as Element).attributes;
const ns = attrs.getNamedItem("block-ns");
if (ns) {
attrs.removeNamedItem("block-ns");
currentNS = ns.value;
}
currentNS ||= (node as Element).namespaceURI;
if (!el) {
el = currentNS
? document.createElementNS(currentNS, tagName)
Expand All @@ -165,6 +160,7 @@ function buildTree(
const fragment = document.createElement("template").content;
fragment.appendChild(el);
}
const attrs = (node as Element).attributes;
for (let i = 0; i < attrs.length; i++) {
const attrName = attrs[i].name;
const attrValue = attrs[i].value;
Expand Down
6 changes: 4 additions & 2 deletions tests/blockdom/block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,14 @@ describe("misc", () => {
});

test("namespace is not propagated to siblings", () => {
const block = createBlock(`<div><svg block-ns="someNameSpace"><g/></svg><div></div></div>`);
const block = createBlock(`<div><svg xmlns="someNameSpace"><g/></svg><div></div></div>`);

const fixture = makeTestFixture();
mount(block(), fixture);

expect(fixture.innerHTML).toBe("<div><svg><g></g></svg><div></div></div>");
expect(fixture.innerHTML).toBe(
'<div><svg xmlns="someNameSpace"><g></g></svg><div></div></div>'
);
expect(fixture.querySelector("svg")!.namespaceURI).toBe("someNameSpace");
expect(fixture.querySelector("g")!.namespaceURI).toBe("someNameSpace");
const allDivs = fixture.querySelectorAll("div");
Expand Down
14 changes: 7 additions & 7 deletions tests/blockdom/namespace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,22 @@ describe("namespace", () => {
expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI);
});

test("namespace can be changed with block-ns", () => {
const block = createBlock(`<tag block-ns="${SVG_URI}"/>`);
test("namespace can be changed with xmlns", () => {
const block = createBlock(`<tag xmlns="${SVG_URI}"/>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<tag></tag>");
expect(fixture.innerHTML).toBe(`<tag xmlns="${SVG_URI}"></tag>`);
expect(fixture.firstElementChild!.namespaceURI).toBe(SVG_URI);
});

test("namespace is kept for children", () => {
const block = createBlock(
`<parent block-ns="${SVG_URI}"><child><subchild/></child><child/></parent>`
`<parent xmlns="${SVG_URI}"><child><subchild/></child><child/></parent>`
);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe(
"<parent><child><subchild></subchild></child><child></child></parent>"
`<parent xmlns="${SVG_URI}"><child><subchild></subchild></child><child></child></parent>`
);
const parent = fixture.firstElementChild!;
const child1 = parent.firstElementChild!;
Expand All @@ -58,10 +58,10 @@ describe("namespace", () => {
});

test("various namespaces in same block", () => {
const block = createBlock(`<none><one block-ns="one"/><two block-ns="two"/></none>`);
const block = createBlock(`<none><one xmlns="one"/><two xmlns="two"/></none>`);
const tree = block();
mount(tree, fixture);
expect(fixture.innerHTML).toBe("<none><one></one><two></two></none>");
expect(fixture.innerHTML).toBe('<none><one xmlns="one"></one><two xmlns="two"></two></none>');
const none = fixture.firstElementChild!;
const one = none.firstElementChild!;
const two = one.nextElementSibling!;
Expand Down
22 changes: 11 additions & 11 deletions tests/compiler/__snapshots__/svg.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`properly support svg add proper namespace to g tags 1`] = `
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </g>\`);
let block1 = createBlock(\`<g xmlns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </g>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
Expand All @@ -18,7 +18,7 @@ exports[`properly support svg add proper namespace to svg 1`] = `
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\" width=\\"100px\\" height=\\"90px\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </svg>\`);
let block1 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"100px\\" height=\\"90px\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/> </svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
Expand All @@ -31,7 +31,7 @@ exports[`properly support svg namespace to g tags not added if already in svg na
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><g/></svg>\`);
let block1 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\"><g/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
Expand All @@ -44,7 +44,7 @@ exports[`properly support svg namespace to svg tags added even if already in svg
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><svg/></svg>\`);
let block1 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\"><svg/></svg>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
Expand All @@ -58,8 +58,8 @@ exports[`properly support svg svg creates new block if it is within html -- 2 1`
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><polygon fill=\\"#000000\\" points=\\"0 0 4 4 8 0\\" transform=\\"translate(5 7)\\"/><block-child-0/></svg>\`);
let block3 = createBlock(\`<path block-ns=\\"http://www.w3.org/2000/svg\\"/>\`);
let block2 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\"><polygon fill=\\"#000000\\" points=\\"0 0 4 4 8 0\\" transform=\\"translate(5 7)\\"/><block-child-0 xmlns=\\"\\"/></svg>\`);
let block3 = createBlock(\`<path xmlns=\\"http://www.w3.org/2000/svg\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let b3;
Expand All @@ -78,7 +78,7 @@ exports[`properly support svg svg creates new block if it is within html 1`] = `
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
let block2 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><polygon fill=\\"#000000\\" points=\\"0 0 4 4 8 0\\" transform=\\"translate(5 7)\\"/></svg>\`);
let block2 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\"><polygon fill=\\"#000000\\" points=\\"0 0 4 4 8 0\\" transform=\\"translate(5 7)\\"/></svg>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = block2();
Expand All @@ -93,7 +93,7 @@ exports[`properly support svg svg namespace added to sub templates if root tag i
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const callTemplate_1 = app.getTemplate(\`path\`);
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
let block1 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\"><block-child-0 xmlns=\\"\\"/></svg>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`);
Expand All @@ -107,7 +107,7 @@ exports[`properly support svg svg namespace added to sub templates if root tag i
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<path block-ns=\\"http://www.w3.org/2000/svg\\"/>\`);
let block1 = createBlock(\`<path xmlns=\\"http://www.w3.org/2000/svg\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
Expand All @@ -120,8 +120,8 @@ exports[`properly support svg svg namespace added to sub-blocks 1`] = `
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
let block2 = createBlock(\`<path block-ns=\\"http://www.w3.org/2000/svg\\"/>\`);
let block1 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\"><block-child-0 xmlns=\\"\\"/></svg>\`);
let block2 = createBlock(\`<path xmlns=\\"http://www.w3.org/2000/svg\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let b2;
Expand Down
15 changes: 10 additions & 5 deletions tests/compiler/svg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ describe("properly support svg", () => {
test("add proper namespace to svg", () => {
const template = `<svg width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </svg>`;
expect(renderToString(template)).toBe(
`<svg width=\"100px\" height=\"90px\"><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </svg>`
`<svg xmlns="http://www.w3.org/2000/svg" width="100px" height="90px"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"></circle> </svg>`
);
});

test("add proper namespace to g tags", () => {
const template = `<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/> </g>`;
expect(renderToString(template)).toBe(
`<g><circle cx=\"50\" cy=\"50\" r=\"4\" stroke=\"green\" stroke-width=\"1\" fill=\"yellow\"></circle> </g>`
`<g xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"></circle> </g>`
);
});

test("namespace to g tags not added if already in svg namespace", () => {
const template = `<svg><g/></svg>`;
expect(renderToString(template)).toBe(`<svg><g></g></svg>`);
expect(renderToString(template)).toBe(`<svg xmlns="http://www.w3.org/2000/svg"><g></g></svg>`);
});

test("namespace to svg tags added even if already in svg namespace", () => {
Expand All @@ -41,8 +41,13 @@ describe("properly support svg", () => {
test("svg namespace added to sub-blocks", () => {
const template = `<svg><path t-if="path"/></svg>`;

expect(renderToString(template, { path: false })).toBe(`<svg></svg>`);
expect(renderToString(template, { path: true })).toBe(`<svg><path></path></svg>`);
expect(renderToString(template, { path: false })).toBe(
`<svg xmlns="http://www.w3.org/2000/svg"></svg>`
);
// Because the path is its own block, it has its own xmlns attribute
expect(renderToString(template, { path: true })).toBe(
`<svg xmlns="http://www.w3.org/2000/svg"><path xmlns="http://www.w3.org/2000/svg"></path></svg>`
);

const bdom = renderToBdom(template, { path: true });
const fixture = makeTestFixture();
Expand Down
4 changes: 2 additions & 2 deletions tests/components/__snapshots__/basics.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,7 @@ exports[`support svg components add proper namespace to svg 1`] = `
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`GComp\`, true, false, false, []);

let block1 = createBlock(\`<svg block-ns=\\"http://www.w3.org/2000/svg\\"><block-child-0/></svg>\`);
let block1 = createBlock(\`<svg xmlns=\\"http://www.w3.org/2000/svg\\"><block-child-0 xmlns=\\"\\"/></svg>\`);

return function template(ctx, node, key = \\"\\") {
const b2 = comp1({}, key + \`__1\`, node, this, null);
Expand All @@ -1262,7 +1262,7 @@ exports[`support svg components add proper namespace to svg 2`] = `
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

let block1 = createBlock(\`<g block-ns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/></g>\`);
let block1 = createBlock(\`<g xmlns=\\"http://www.w3.org/2000/svg\\"><circle cx=\\"50\\" cy=\\"50\\" r=\\"4\\" stroke=\\"green\\" stroke-width=\\"1\\" fill=\\"yellow\\"/></g>\`);

return function template(ctx, node, key = \\"\\") {
return block1();
Expand Down
2 changes: 1 addition & 1 deletion tests/components/basics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,7 @@ describe("support svg components", () => {
await mount(Svg, fixture);

expect(fixture.innerHTML).toBe(
'<svg><g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"></circle></g></svg>'
'<svg xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"></circle></g></svg>'
);
});
});
Expand Down

0 comments on commit 7108296

Please sign in to comment.