From b25e988628b1f1801f8997f07b4ff48779332b27 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Fri, 4 Aug 2023 10:44:47 +0200 Subject: [PATCH] [FIX] compiler: fix xmlns attribute not being set correctly in firefox 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 --- src/compiler/code_generator.ts | 10 ++++----- src/compiler/parser.ts | 14 +++++++----- src/runtime/blockdom/block_compiler.ts | 8 ++----- tests/blockdom/block.test.ts | 6 +++-- tests/blockdom/namespace.test.ts | 14 ++++++------ tests/compiler/__snapshots__/svg.test.ts.snap | 22 +++++++++---------- tests/compiler/svg.test.ts | 15 ++++++++----- .../__snapshots__/basics.test.ts.snap | 4 ++-- tests/components/basics.test.ts | 2 +- 9 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 37051bea..38b51973 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -585,11 +585,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; @@ -718,7 +713,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); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 555835a0..3a4cbb0e 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -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 { @@ -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"); @@ -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}'`); @@ -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 { diff --git a/src/runtime/blockdom/block_compiler.ts b/src/runtime/blockdom/block_compiler.ts index cbc91e7d..13435cbb 100644 --- a/src/runtime/blockdom/block_compiler.ts +++ b/src/runtime/blockdom/block_compiler.ts @@ -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) @@ -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; diff --git a/tests/blockdom/block.test.ts b/tests/blockdom/block.test.ts index 0576efa5..7a355167 100644 --- a/tests/blockdom/block.test.ts +++ b/tests/blockdom/block.test.ts @@ -244,12 +244,14 @@ describe("misc", () => { }); test("namespace is not propagated to siblings", () => { - const block = createBlock(`
`); + const block = createBlock(`
`); const fixture = makeTestFixture(); mount(block(), fixture); - expect(fixture.innerHTML).toBe("
"); + expect(fixture.innerHTML).toBe( + '
' + ); expect(fixture.querySelector("svg")!.namespaceURI).toBe("someNameSpace"); expect(fixture.querySelector("g")!.namespaceURI).toBe("someNameSpace"); const allDivs = fixture.querySelectorAll("div"); diff --git a/tests/blockdom/namespace.test.ts b/tests/blockdom/namespace.test.ts index e49d83c6..0d622ca4 100644 --- a/tests/blockdom/namespace.test.ts +++ b/tests/blockdom/namespace.test.ts @@ -30,22 +30,22 @@ describe("namespace", () => { expect(fixture.firstElementChild!.namespaceURI).toBe(XHTML_URI); }); - test("namespace can be changed with block-ns", () => { - const block = createBlock(``); + test("namespace can be changed with xmlns", () => { + const block = createBlock(``); const tree = block(); mount(tree, fixture); - expect(fixture.innerHTML).toBe(""); + 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!; @@ -58,10 +58,10 @@ describe("namespace", () => { }); test("various namespaces in same block", () => { - const block = createBlock(``); + const block = createBlock(``); const tree = block(); mount(tree, fixture); - expect(fixture.innerHTML).toBe(""); + expect(fixture.innerHTML).toBe(''); const none = fixture.firstElementChild!; const one = none.firstElementChild!; const two = one.nextElementSibling!; diff --git a/tests/compiler/__snapshots__/svg.test.ts.snap b/tests/compiler/__snapshots__/svg.test.ts.snap index f8c71063..b4cef8bd 100644 --- a/tests/compiler/__snapshots__/svg.test.ts.snap +++ b/tests/compiler/__snapshots__/svg.test.ts.snap @@ -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(\` \`); + let block1 = createBlock(\` \`); return function template(ctx, node, key = \\"\\") { return block1(); @@ -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(\` \`); + let block1 = createBlock(\` \`); return function template(ctx, node, key = \\"\\") { return block1(); @@ -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(\`\`); + let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { return block1(); @@ -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(\`\`); + let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { return block1(); @@ -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(\`
\`); - let block2 = createBlock(\`\`); - let block3 = createBlock(\`\`); + let block2 = createBlock(\`\`); + let block3 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { let b3; @@ -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(\`
\`); - let block2 = createBlock(\`\`); + let block2 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { const b2 = block2(); @@ -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(\`\`); + let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { const b2 = callTemplate_1.call(this, ctx, node, key + \`__1\`); @@ -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(\`\`); + let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { return block1(); @@ -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(\`\`); - let block2 = createBlock(\`\`); + let block1 = createBlock(\`\`); + let block2 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { let b2; diff --git a/tests/compiler/svg.test.ts b/tests/compiler/svg.test.ts index 9c5cdad2..5ead0596 100644 --- a/tests/compiler/svg.test.ts +++ b/tests/compiler/svg.test.ts @@ -9,20 +9,20 @@ describe("properly support svg", () => { test("add proper namespace to svg", () => { const template = ` `; expect(renderToString(template)).toBe( - ` ` + ` ` ); }); test("add proper namespace to g tags", () => { const template = ` `; expect(renderToString(template)).toBe( - ` ` + ` ` ); }); test("namespace to g tags not added if already in svg namespace", () => { const template = ``; - expect(renderToString(template)).toBe(``); + expect(renderToString(template)).toBe(``); }); test("namespace to svg tags added even if already in svg namespace", () => { @@ -41,8 +41,13 @@ describe("properly support svg", () => { test("svg namespace added to sub-blocks", () => { const template = ``; - expect(renderToString(template, { path: false })).toBe(``); - expect(renderToString(template, { path: true })).toBe(``); + expect(renderToString(template, { path: false })).toBe( + `` + ); + // Because the path is its own block, it has its own xmlns attribute + expect(renderToString(template, { path: true })).toBe( + `` + ); const bdom = renderToBdom(template, { path: true }); const fixture = makeTestFixture(); diff --git a/tests/components/__snapshots__/basics.test.ts.snap b/tests/components/__snapshots__/basics.test.ts.snap index f72b260c..699a8fb7 100644 --- a/tests/components/__snapshots__/basics.test.ts.snap +++ b/tests/components/__snapshots__/basics.test.ts.snap @@ -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(\`\`); + let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { const b2 = comp1({}, key + \`__1\`, node, this, null); @@ -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(\`\`); + let block1 = createBlock(\`\`); return function template(ctx, node, key = \\"\\") { return block1(); diff --git a/tests/components/basics.test.ts b/tests/components/basics.test.ts index 3dfe537c..6ddd5d65 100644 --- a/tests/components/basics.test.ts +++ b/tests/components/basics.test.ts @@ -1024,7 +1024,7 @@ describe("support svg components", () => { await mount(Svg, fixture); expect(fixture.innerHTML).toBe( - '' + '' ); }); });