From 16bbf0316b236dcf9b57b5b8aa92145cef917e31 Mon Sep 17 00:00:00 2001 From: Gudine Date: Thu, 29 May 2025 17:56:04 -0300 Subject: [PATCH 1/9] Create helper function `htmlToFragment` --- esm/shared/utils.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esm/shared/utils.js b/esm/shared/utils.js index 9074afca..6c402823 100644 --- a/esm/shared/utils.js +++ b/esm/shared/utils.js @@ -38,3 +38,14 @@ export const setAdjacent = (prev, next) => { if (next) next[PREV] = prev; }; + +export const htmlToFragment = (ownerDocument, html) => { + const fragment = ownerDocument.createDocumentFragment(); + + const elem = ownerDocument.createElement(''); + elem.innerHTML = html; + + for (const node of elem.childNodes) fragment.appendChild(node.cloneNode(true)); + + return fragment; +}; \ No newline at end of file From 5713992c28c83abe79560b3bb7883f117db29e20 Mon Sep 17 00:00:00 2001 From: Gudine Date: Thu, 29 May 2025 18:08:15 -0300 Subject: [PATCH 2/9] Fix insertAdjacentHTML not working on XML (and potentially SVG) documents --- esm/interface/element.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esm/interface/element.js b/esm/interface/element.js index e7f927e1..c98f5f7d 100644 --- a/esm/interface/element.js +++ b/esm/interface/element.js @@ -23,6 +23,7 @@ import { } from '../shared/symbols.js'; import { + htmlToFragment, ignoreCase, knownAdjacent, localCase, @@ -384,9 +385,7 @@ export class Element extends ParentNode { } insertAdjacentHTML(position, html) { - const template = this.ownerDocument.createElement('template'); - template.innerHTML = html; - this.insertAdjacentElement(position, template.content); + this.insertAdjacentElement(position, htmlToFragment(this.ownerDocument, html)); } insertAdjacentText(position, text) { From 4b09c3e11c76f61a18ab6a451056e3d03a96409b Mon Sep 17 00:00:00 2001 From: Gudine Date: Thu, 29 May 2025 18:08:31 -0300 Subject: [PATCH 3/9] Fix createContextualFragment not working on XML (and potentially SVG) documents --- esm/interface/range.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esm/interface/range.js b/esm/interface/range.js index 45defd5e..1967ef4e 100644 --- a/esm/interface/range.js +++ b/esm/interface/range.js @@ -4,7 +4,7 @@ import {END, NEXT, PREV, START} from '../shared/symbols.js'; import {SVGElement} from '../svg/element.js'; -import {getEnd, setAdjacent} from '../shared/utils.js'; +import {getEnd, htmlToFragment, setAdjacent} from '../shared/utils.js'; const deleteContents = ({[START]: start, [END]: end}, fragment = null) => { setAdjacent(start[PREV], end[NEXT]); @@ -101,9 +101,7 @@ export class Range { const { commonAncestorContainer: doc } = this; const isSVG = 'ownerSVGElement' in doc; const document = isSVG ? doc.ownerDocument : doc; - const template = document.createElement('template'); - template.innerHTML = html; - let {content} = template; + let content = htmlToFragment(document, html); if (isSVG) { const childNodes = [...content.childNodes]; content = document.createDocumentFragment(); From a27c22b00a8f037f8d701c30f98221131b3ca48e Mon Sep 17 00:00:00 2001 From: Gudine Date: Thu, 29 May 2025 18:09:55 -0300 Subject: [PATCH 4/9] Move insertAdjacent... tests from html to general elements, and cover XML --- test/html/element.js | 16 ---------------- test/interface/element.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/test/html/element.js b/test/html/element.js index 84198acb..3c3fedd8 100644 --- a/test/html/element.js +++ b/test/html/element.js @@ -113,22 +113,6 @@ node.nonce = 'abc'; assert(node.nonce, 'abc', 'yes nonce'); node = document.createElement('div'); -node.innerHTML = '

!

'; -assert(node.innerHTML, '

!

', 'innerHTML'); -node.insertAdjacentHTML('beforebegin', 'beforebegin'); -node.insertAdjacentHTML('afterend', 'afterend'); -assert(node.toString(), '

!

', 'no element, no before/after'); -node.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin'); -assert(node.toString(), '
beforebegin

!

', 'beforebegin works'); -node.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin'); -assert(node.toString(), '
beforebegin

afterbegin!

', 'afterbegin works'); -node.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend'); -assert(node.toString(), '
beforebegin

afterbegin!beforeend

', 'beforeend works'); -node.firstElementChild.insertAdjacentHTML('afterend', 'afterend'); -assert(node.toString(), '
beforebegin

afterbegin!beforeend

afterend
', 'afterend works'); - -node.firstElementChild.insertAdjacentText('afterend', ''); -assert(node.toString(), '
beforebegin

afterbegin!beforeend

<OK>afterend
', 'insertAdjacentText works'); node.setAttribute('a', '1'); assert(node.attributes[0].name, 'a') diff --git a/test/interface/element.js b/test/interface/element.js index 7844f9b7..801e812e 100644 --- a/test/interface/element.js +++ b/test/interface/element.js @@ -31,6 +31,24 @@ assert(htmlDoc.firstChild.getAttribute('content-desc'), ''); assert(htmlDoc.firstChild.outerHTML, ''); assert(htmlDoc.innerHTML, ''); +const htmlNode = htmlDoc.ownerDocument.createElement('div'); +htmlNode.innerHTML = '

!

'; +assert(htmlNode.innerHTML, '

!

', 'innerHTML'); +htmlNode.insertAdjacentHTML('beforebegin', 'beforebegin'); +htmlNode.insertAdjacentHTML('afterend', 'afterend'); +assert(htmlNode.toString(), '

!

', 'no element, no before/after'); +htmlNode.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin'); +assert(htmlNode.toString(), '
beforebegin

!

', 'beforebegin works'); +htmlNode.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin'); +assert(htmlNode.toString(), '
beforebegin

afterbegin!

', 'afterbegin works'); +htmlNode.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend'); +assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend

', 'beforeend works'); +htmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend'); +assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend

afterend
', 'afterend works'); + +htmlNode.firstElementChild.insertAdjacentText('afterend', ''); +assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend

<OK>afterend
', 'insertAdjacentText works'); + const htmlDocWithEmptyAttrFromSet = parser.parseFromString(`
`, 'text/html').documentElement; // attribute is in emptyAttributes set is empty assert(htmlDocWithEmptyAttrFromSet.firstChild.getAttribute('style'), ''); @@ -48,6 +66,24 @@ assert(xmlDoc.firstChild.getAttribute('content-desc'), ''); assert(xmlDoc.firstChild.outerHTML, ''); assert(xmlDoc.innerHTML, ''); +const xmlNode = xmlDoc.ownerDocument.createElement('div'); +xmlNode.innerHTML = '

!

'; +assert(xmlNode.innerHTML, '

!

', 'innerHTML'); +xmlNode.insertAdjacentHTML('beforebegin', 'beforebegin'); +xmlNode.insertAdjacentHTML('afterend', 'afterend'); +assert(xmlNode.toString(), '

!

', 'no element, no before/after'); +xmlNode.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin'); +assert(xmlNode.toString(), '
beforebegin

!

', 'beforebegin works'); +xmlNode.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin'); +assert(xmlNode.toString(), '
beforebegin

afterbegin!

', 'afterbegin works'); +xmlNode.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend'); +assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend

', 'beforeend works'); +xmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend'); +assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend

afterend
', 'afterend works'); + +xmlNode.firstElementChild.insertAdjacentText('afterend', ''); +assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend

<OK>afterend
', 'insertAdjacentText works'); + const xmlDocWithEmptyAttrFromSet = parser.parseFromString(``, 'text/xml').documentElement;// attribute is in emptyAttributes set is empty (even for XML) assert(xmlDocWithEmptyAttrFromSet.firstChild.getAttribute('style'), ''); assert(xmlDocWithEmptyAttrFromSet.firstChild.outerHTML, ''); From a09a0c9ef61e3f0e3d55e98c76d644c5e7baa669 Mon Sep 17 00:00:00 2001 From: Gudine Date: Thu, 29 May 2025 18:11:58 -0300 Subject: [PATCH 5/9] Add test for createContextualFragment in SVG documents --- test/interface/range.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/interface/range.js b/test/interface/range.js index dd1634ea..cb894cd3 100644 --- a/test/interface/range.js +++ b/test/interface/range.js @@ -1,6 +1,6 @@ const assert = require('../assert.js').for('Range'); -const {parseHTML} = global[Symbol.for('linkedom')]; +const {parseHTML, DOMParser} = global[Symbol.for('linkedom')]; const {document} = parseHTML('
abc
'); @@ -70,3 +70,11 @@ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); range.selectNodeContents(svg); const rect = range.createContextualFragment('').childNodes[0]; assert('ownerSVGElement' in rect, true, 'createContextualFragment(SVG)'); + +{ + const svgDocument = (new DOMParser).parseFromString('', 'image/svg+xml'); + + let range = svgDocument.createRange(); + let contextual = range.createContextualFragment('
hi
'); + assert(contextual.toString(), '<#document-fragment>
hi
', 'createContextualFragment'); +} From 95abcd1a05cc207ea3606db5cfd072a25493b9d4 Mon Sep 17 00:00:00 2001 From: Gudine Date: Thu, 29 May 2025 18:12:49 -0300 Subject: [PATCH 6/9] Build --- cjs/interface/element.js | 5 ++--- cjs/interface/range.js | 6 ++---- cjs/shared/utils.js | 12 ++++++++++++ types/esm/shared/utils.d.ts | 1 + worker.js | 19 +++++++++++++------ 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/cjs/interface/element.js b/cjs/interface/element.js index 34cb95ba..5d513d6a 100644 --- a/cjs/interface/element.js +++ b/cjs/interface/element.js @@ -21,6 +21,7 @@ const { } = require('../shared/symbols.js'); const { + htmlToFragment, ignoreCase, knownAdjacent, localCase, @@ -382,9 +383,7 @@ class Element extends ParentNode { } insertAdjacentHTML(position, html) { - const template = this.ownerDocument.createElement('template'); - template.innerHTML = html; - this.insertAdjacentElement(position, template.content); + this.insertAdjacentElement(position, htmlToFragment(this.ownerDocument, html)); } insertAdjacentText(position, text) { diff --git a/cjs/interface/range.js b/cjs/interface/range.js index 3c3e3895..7f185120 100644 --- a/cjs/interface/range.js +++ b/cjs/interface/range.js @@ -5,7 +5,7 @@ const {END, NEXT, PREV, START} = require('../shared/symbols.js'); const {SVGElement} = require('../svg/element.js'); -const {getEnd, setAdjacent} = require('../shared/utils.js'); +const {getEnd, htmlToFragment, setAdjacent} = require('../shared/utils.js'); const deleteContents = ({[START]: start, [END]: end}, fragment = null) => { setAdjacent(start[PREV], end[NEXT]); @@ -102,9 +102,7 @@ class Range { const { commonAncestorContainer: doc } = this; const isSVG = 'ownerSVGElement' in doc; const document = isSVG ? doc.ownerDocument : doc; - const template = document.createElement('template'); - template.innerHTML = html; - let {content} = template; + let content = htmlToFragment(document, html); if (isSVG) { const childNodes = [...content.childNodes]; content = document.createDocumentFragment(); diff --git a/cjs/shared/utils.js b/cjs/shared/utils.js index 339b3b9a..1d20cc69 100644 --- a/cjs/shared/utils.js +++ b/cjs/shared/utils.js @@ -47,3 +47,15 @@ const setAdjacent = (prev, next) => { next[PREV] = prev; }; exports.setAdjacent = setAdjacent; + +const htmlToFragment = (ownerDocument, html) => { + const fragment = ownerDocument.createDocumentFragment(); + + const elem = ownerDocument.createElement(''); + elem.innerHTML = html; + + for (const node of elem.childNodes) fragment.appendChild(node.cloneNode(true)); + + return fragment; +}; +exports.htmlToFragment = htmlToFragment; \ No newline at end of file diff --git a/types/esm/shared/utils.d.ts b/types/esm/shared/utils.d.ts index 46afbdbb..a303715e 100644 --- a/types/esm/shared/utils.d.ts +++ b/types/esm/shared/utils.d.ts @@ -12,4 +12,5 @@ export function localCase({ localName, ownerDocument }: { ownerDocument: any; }): any; export function setAdjacent(prev: any, next: any): void; +export function htmlToFragment(ownerDocument: any, html: any): any; declare const $String: StringConstructor; diff --git a/worker.js b/worker.js index 858ae068..d5aae529 100644 --- a/worker.js +++ b/worker.js @@ -3893,6 +3893,17 @@ const setAdjacent = (prev, next) => { next[PREV] = prev; }; +const htmlToFragment = (ownerDocument, html) => { + const fragment = ownerDocument.createDocumentFragment(); + + const elem = ownerDocument.createElement(''); + elem.innerHTML = html; + + for (const node of elem.childNodes) fragment.appendChild(node.cloneNode(true)); + + return fragment; +}; + const shadowRoots = new WeakMap; let reactive = false; @@ -7969,9 +7980,7 @@ let Element$1 = class Element extends ParentNode { } insertAdjacentHTML(position, html) { - const template = this.ownerDocument.createElement('template'); - template.innerHTML = html; - this.insertAdjacentElement(position, template.content); + this.insertAdjacentElement(position, htmlToFragment(this.ownerDocument, html)); } insertAdjacentText(position, text) { @@ -12147,9 +12156,7 @@ class Range { const { commonAncestorContainer: doc } = this; const isSVG = 'ownerSVGElement' in doc; const document = isSVG ? doc.ownerDocument : doc; - const template = document.createElement('template'); - template.innerHTML = html; - let {content} = template; + let content = htmlToFragment(document, html); if (isSVG) { const childNodes = [...content.childNodes]; content = document.createDocumentFragment(); From b81bf847fc777c6bb9769509c29c7f3a85ef74e1 Mon Sep 17 00:00:00 2001 From: Gudine Date: Fri, 30 May 2025 12:22:18 -0300 Subject: [PATCH 7/9] Improve performance of htmlToFragment, and added JSDoc --- esm/shared/utils.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/esm/shared/utils.js b/esm/shared/utils.js index 6c402823..50946b2e 100644 --- a/esm/shared/utils.js +++ b/esm/shared/utils.js @@ -39,13 +39,26 @@ export const setAdjacent = (prev, next) => { next[PREV] = prev; }; +/** + * @param {import("../interface/document.js").Document} ownerDocument + * @param {string} html + * @return {import("../interface/document-fragment.js").DocumentFragment} + */ export const htmlToFragment = (ownerDocument, html) => { const fragment = ownerDocument.createDocumentFragment(); const elem = ownerDocument.createElement(''); elem.innerHTML = html; + const { firstChild, lastChild } = elem; - for (const node of elem.childNodes) fragment.appendChild(node.cloneNode(true)); + if (firstChild) { + knownSegment(fragment, firstChild, lastChild, fragment[END]); + + let child = firstChild; + do { + child.parentNode = fragment; + } while (child !== lastChild && (child = getEnd(child)[NEXT])); + } return fragment; }; \ No newline at end of file From cdd544fd8413bb63e97055a01b2c8bff7c654e8b Mon Sep 17 00:00:00 2001 From: Gudine Date: Fri, 30 May 2025 12:22:56 -0300 Subject: [PATCH 8/9] Add test to check insertAdjacentHTML adding multiple elements --- test/interface/element.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/interface/element.js b/test/interface/element.js index 801e812e..bff3f829 100644 --- a/test/interface/element.js +++ b/test/interface/element.js @@ -46,8 +46,11 @@ assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend

', htmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend'); assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend

afterend
', 'afterend works'); +htmlNode.firstElementChild.insertAdjacentHTML('beforeend', '12'); +assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend12

afterend
', 'multiple html works'); + htmlNode.firstElementChild.insertAdjacentText('afterend', ''); -assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend

<OK>afterend
', 'insertAdjacentText works'); +assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend12

<OK>afterend
', 'insertAdjacentText works'); const htmlDocWithEmptyAttrFromSet = parser.parseFromString(`
`, 'text/html').documentElement; // attribute is in emptyAttributes set is empty @@ -81,8 +84,11 @@ assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend

', xmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend'); assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend

afterend
', 'afterend works'); +xmlNode.firstElementChild.insertAdjacentHTML('beforeend', '12'); +assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend12

afterend
', 'multiple html works'); + xmlNode.firstElementChild.insertAdjacentText('afterend', ''); -assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend

<OK>afterend
', 'insertAdjacentText works'); +assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend12

<OK>afterend
', 'insertAdjacentText works'); const xmlDocWithEmptyAttrFromSet = parser.parseFromString(``, 'text/xml').documentElement;// attribute is in emptyAttributes set is empty (even for XML) assert(xmlDocWithEmptyAttrFromSet.firstChild.getAttribute('style'), ''); From 83439bb241edf4b06896429f1a4b447f8b74d3fb Mon Sep 17 00:00:00 2001 From: Gudine Date: Fri, 30 May 2025 12:24:39 -0300 Subject: [PATCH 9/9] Build --- cjs/shared/utils.js | 15 ++++++++++++++- types/esm/interface/range.d.ts | 2 +- types/esm/shared/utils.d.ts | 2 +- worker.js | 15 ++++++++++++++- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/cjs/shared/utils.js b/cjs/shared/utils.js index 1d20cc69..1dfb969e 100644 --- a/cjs/shared/utils.js +++ b/cjs/shared/utils.js @@ -48,13 +48,26 @@ const setAdjacent = (prev, next) => { }; exports.setAdjacent = setAdjacent; +/** + * @param {import("../interface/document.js").Document} ownerDocument + * @param {string} html + * @return {import("../interface/document-fragment.js").DocumentFragment} + */ const htmlToFragment = (ownerDocument, html) => { const fragment = ownerDocument.createDocumentFragment(); const elem = ownerDocument.createElement(''); elem.innerHTML = html; + const { firstChild, lastChild } = elem; - for (const node of elem.childNodes) fragment.appendChild(node.cloneNode(true)); + if (firstChild) { + knownSegment(fragment, firstChild, lastChild, fragment[END]); + + let child = firstChild; + do { + child.parentNode = fragment; + } while (child !== lastChild && (child = getEnd(child)[NEXT])); + } return fragment; }; diff --git a/types/esm/interface/range.d.ts b/types/esm/interface/range.d.ts index fd97cb3f..ca51a006 100644 --- a/types/esm/interface/range.d.ts +++ b/types/esm/interface/range.d.ts @@ -14,7 +14,7 @@ export class Range implements globalThis.Range { cloneContents(): any; deleteContents(): void; extractContents(): any; - createContextualFragment(html: any): any; + createContextualFragment(html: any): import("./document-fragment.js").DocumentFragment; cloneRange(): Range; [START]: any; [END]: any; diff --git a/types/esm/shared/utils.d.ts b/types/esm/shared/utils.d.ts index a303715e..827f1de8 100644 --- a/types/esm/shared/utils.d.ts +++ b/types/esm/shared/utils.d.ts @@ -12,5 +12,5 @@ export function localCase({ localName, ownerDocument }: { ownerDocument: any; }): any; export function setAdjacent(prev: any, next: any): void; -export function htmlToFragment(ownerDocument: any, html: any): any; +export function htmlToFragment(ownerDocument: import("../interface/document.js").Document, html: string): import("../interface/document-fragment.js").DocumentFragment; declare const $String: StringConstructor; diff --git a/worker.js b/worker.js index d5aae529..d7720bdd 100644 --- a/worker.js +++ b/worker.js @@ -3893,13 +3893,26 @@ const setAdjacent = (prev, next) => { next[PREV] = prev; }; +/** + * @param {import("../interface/document.js").Document} ownerDocument + * @param {string} html + * @return {import("../interface/document-fragment.js").DocumentFragment} + */ const htmlToFragment = (ownerDocument, html) => { const fragment = ownerDocument.createDocumentFragment(); const elem = ownerDocument.createElement(''); elem.innerHTML = html; + const { firstChild, lastChild } = elem; + + if (firstChild) { + knownSegment(fragment, firstChild, lastChild, fragment[END]); - for (const node of elem.childNodes) fragment.appendChild(node.cloneNode(true)); + let child = firstChild; + do { + child.parentNode = fragment; + } while (child !== lastChild && (child = getEnd(child)[NEXT])); + } return fragment; };