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..1dfb969e 100644 --- a/cjs/shared/utils.js +++ b/cjs/shared/utils.js @@ -47,3 +47,28 @@ const setAdjacent = (prev, next) => { next[PREV] = prev; }; 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; + + if (firstChild) { + knownSegment(fragment, firstChild, lastChild, fragment[END]); + + let child = firstChild; + do { + child.parentNode = fragment; + } while (child !== lastChild && (child = getEnd(child)[NEXT])); + } + + return fragment; +}; +exports.htmlToFragment = htmlToFragment; \ No newline at end of file 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) { 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(); diff --git a/esm/shared/utils.js b/esm/shared/utils.js index 9074afca..50946b2e 100644 --- a/esm/shared/utils.js +++ b/esm/shared/utils.js @@ -38,3 +38,27 @@ export const setAdjacent = (prev, next) => { if (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; + + 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 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..bff3f829 100644 --- a/test/interface/element.js +++ b/test/interface/element.js @@ -31,6 +31,27 @@ 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.insertAdjacentHTML('beforeend', '12'); +assert(htmlNode.toString(), '
beforebegin

afterbegin!beforeend12

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

afterbegin!beforeend12

<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 +69,27 @@ 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.insertAdjacentHTML('beforeend', '12'); +assert(xmlNode.toString(), '
beforebegin

afterbegin!beforeend12

afterend
', 'multiple html works'); + +xmlNode.firstElementChild.insertAdjacentText('afterend', ''); +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'), ''); assert(xmlDocWithEmptyAttrFromSet.firstChild.outerHTML, ''); 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'); +} 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 46afbdbb..827f1de8 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: 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 858ae068..d7720bdd 100644 --- a/worker.js +++ b/worker.js @@ -3893,6 +3893,30 @@ 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]); + + let child = firstChild; + do { + child.parentNode = fragment; + } while (child !== lastChild && (child = getEnd(child)[NEXT])); + } + + return fragment; +}; + const shadowRoots = new WeakMap; let reactive = false; @@ -7969,9 +7993,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 +12169,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();