From 563809ca9d2b93b743245c0f05b6fdfa6af9893c Mon Sep 17 00:00:00 2001 From: Jonas Porto Date: Thu, 15 Oct 2026 21:11:07 -0300 Subject: [PATCH] fix: parse innerHTML of an SVG element as SVG (#326) --- cjs/mixin/inner-html.js | 19 +++++++++++++++++-- esm/mixin/inner-html.js | 19 +++++++++++++++++-- test/svg/element.js | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/cjs/mixin/inner-html.js b/cjs/mixin/inner-html.js index ec106d80..9d3e3165 100644 --- a/cjs/mixin/inner-html.js +++ b/cjs/mixin/inner-html.js @@ -1,6 +1,6 @@ 'use strict'; const {ELEMENT_NODE, DOCUMENT_FRAGMENT_NODE} = require('../shared/constants.js'); -const {CUSTOM_ELEMENTS} = require('../shared/symbols.js'); +const {CUSTOM_ELEMENTS, END, NEXT} = require('../shared/symbols.js'); const {parseFromString} = require('../shared/parse-from-string.js'); const {ignoreCase} = require('../shared/utils.js'); @@ -21,7 +21,22 @@ const setInnerHtml = (node, html) => { const {constructor} = ownerDocument; const document = new constructor; document[CUSTOM_ELEMENTS] = ownerDocument[CUSTOM_ELEMENTS]; - const {childNodes} = parseFromString(document, ignoreCase(node), html); + const isHTML = ignoreCase(node); + // the content of an SVG element is SVG: closes, children are SVGElement + const isSVG = isHTML && 'ownerSVGElement' in node; + let {childNodes} = parseFromString(document, isHTML, isSVG ? `${html}` : html); + + if (isSVG) { + const svg = childNodes[0]; + const ownerSVGElement = node.localName === 'svg' ? node : node.ownerSVGElement; + let {[NEXT]: next, [END]: end} = svg; + while (next !== end) { + if (next.nodeType === ELEMENT_NODE) + next.ownerSVGElement = ownerSVGElement; + next = next[NEXT]; + } + childNodes = svg.childNodes; + } node.replaceChildren(...childNodes.map(setOwnerDocument, ownerDocument)); }; diff --git a/esm/mixin/inner-html.js b/esm/mixin/inner-html.js index ac37e8f7..7c25809b 100644 --- a/esm/mixin/inner-html.js +++ b/esm/mixin/inner-html.js @@ -1,5 +1,5 @@ import {ELEMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/constants.js'; -import {CUSTOM_ELEMENTS} from '../shared/symbols.js'; +import {CUSTOM_ELEMENTS, END, NEXT} from '../shared/symbols.js'; import {parseFromString} from '../shared/parse-from-string.js'; import {ignoreCase} from '../shared/utils.js'; @@ -19,7 +19,22 @@ export const setInnerHtml = (node, html) => { const {constructor} = ownerDocument; const document = new constructor; document[CUSTOM_ELEMENTS] = ownerDocument[CUSTOM_ELEMENTS]; - const {childNodes} = parseFromString(document, ignoreCase(node), html); + const isHTML = ignoreCase(node); + // the content of an SVG element is SVG: closes, children are SVGElement + const isSVG = isHTML && 'ownerSVGElement' in node; + let {childNodes} = parseFromString(document, isHTML, isSVG ? `${html}` : html); + + if (isSVG) { + const svg = childNodes[0]; + const ownerSVGElement = node.localName === 'svg' ? node : node.ownerSVGElement; + let {[NEXT]: next, [END]: end} = svg; + while (next !== end) { + if (next.nodeType === ELEMENT_NODE) + next.ownerSVGElement = ownerSVGElement; + next = next[NEXT]; + } + childNodes = svg.childNodes; + } node.replaceChildren(...childNodes.map(setOwnerDocument, ownerDocument)); }; diff --git a/test/svg/element.js b/test/svg/element.js index 4f5259ca..97a55d39 100644 --- a/test/svg/element.js +++ b/test/svg/element.js @@ -46,4 +46,20 @@ assert(svg.getAttribute('class'), 'a b c'); svg.setAttribute('class', 'd e'); assert(svg.getAttribute('class'), 'd e'); -assert(svg.namespaceURI, 'http://www.w3.org/2000/svg'); \ No newline at end of file +assert(svg.namespaceURI, 'http://www.w3.org/2000/svg'); + +// https://github.com/WebReflection/linkedom/issues/326 +({document} = parseHTML('')); +svg = document.querySelector('svg'); +svg.innerHTML = ''; +assert(svg.childNodes.length, 2, 'self-closing children are siblings'); +assert(svg.firstChild instanceof SVGElement, true, 'innerHTML children are SVG elements'); +assert(svg.firstChild.ownerSVGElement, svg, 'innerHTML children ownerSVGElement'); +assert(svg.innerHTML, ''); + +svg.innerHTML = ''; +const g = svg.firstChild; +g.innerHTML = ''; +assert(g.toString(), '', 'nested innerHTML'); +assert(g.firstChild.ownerSVGElement, svg, 'nested innerHTML ownerSVGElement'); +assert(g.lastChild.firstChild.ownerSVGElement, svg, 'deep innerHTML ownerSVGElement');