Skip to content

Fix insertAdjacentHTML and createContextualFragment not working on non-HTML documents #307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 4, 2025
5 changes: 2 additions & 3 deletions cjs/interface/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
} = require('../shared/symbols.js');

const {
htmlToFragment,
ignoreCase,
knownAdjacent,
localCase,
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 2 additions & 4 deletions cjs/interface/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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();
Expand Down
25 changes: 25 additions & 0 deletions cjs/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 2 additions & 3 deletions esm/interface/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../shared/symbols.js';

import {
htmlToFragment,
ignoreCase,
knownAdjacent,
localCase,
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 2 additions & 4 deletions esm/interface/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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();
Expand Down
24 changes: 24 additions & 0 deletions esm/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
16 changes: 0 additions & 16 deletions test/html/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,6 @@ node.nonce = 'abc';
assert(node.nonce, 'abc', 'yes nonce');

node = document.createElement('div');
node.innerHTML = '<p>!</p>';
assert(node.innerHTML, '<p>!</p>', 'innerHTML');
node.insertAdjacentHTML('beforebegin', 'beforebegin');
node.insertAdjacentHTML('afterend', 'afterend');
assert(node.toString(), '<div><p>!</p></div>', 'no element, no before/after');
node.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin');
assert(node.toString(), '<div>beforebegin<p>!</p></div>', 'beforebegin works');
node.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin');
assert(node.toString(), '<div>beforebegin<p>afterbegin!</p></div>', 'afterbegin works');
node.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend');
assert(node.toString(), '<div>beforebegin<p>afterbegin!beforeend</p></div>', 'beforeend works');
node.firstElementChild.insertAdjacentHTML('afterend', 'afterend');
assert(node.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>afterend</div>', 'afterend works');

node.firstElementChild.insertAdjacentText('afterend', '<OK>');
assert(node.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>&lt;OK&gt;afterend</div>', 'insertAdjacentText works');

node.setAttribute('a', '1');
assert(node.attributes[0].name, 'a')
Expand Down
42 changes: 42 additions & 0 deletions test/interface/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@ assert(htmlDoc.firstChild.getAttribute('content-desc'), '');
assert(htmlDoc.firstChild.outerHTML, '<span content-desc=""></span>');
assert(htmlDoc.innerHTML, '<span content-desc=""></span>');

const htmlNode = htmlDoc.ownerDocument.createElement('div');
htmlNode.innerHTML = '<p>!</p>';
assert(htmlNode.innerHTML, '<p>!</p>', 'innerHTML');
htmlNode.insertAdjacentHTML('beforebegin', 'beforebegin');
htmlNode.insertAdjacentHTML('afterend', 'afterend');
assert(htmlNode.toString(), '<div><p>!</p></div>', 'no element, no before/after');
htmlNode.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin');
assert(htmlNode.toString(), '<div>beforebegin<p>!</p></div>', 'beforebegin works');
htmlNode.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin');
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!</p></div>', 'afterbegin works');
htmlNode.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend');
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p></div>', 'beforeend works');
htmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend');
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>afterend</div>', 'afterend works');

htmlNode.firstElementChild.insertAdjacentHTML('beforeend', '<i>1</i><i>2</i>');
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>afterend</div>', 'multiple html works');

htmlNode.firstElementChild.insertAdjacentText('afterend', '<OK>');
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>&lt;OK&gt;afterend</div>', 'insertAdjacentText works');

const htmlDocWithEmptyAttrFromSet = parser.parseFromString(`<div><span style=""/></div>`, 'text/html').documentElement; // attribute is in emptyAttributes set is empty

assert(htmlDocWithEmptyAttrFromSet.firstChild.getAttribute('style'), '');
Expand All @@ -48,6 +69,27 @@ assert(xmlDoc.firstChild.getAttribute('content-desc'), '');
assert(xmlDoc.firstChild.outerHTML, '<android.view.View content-desc="" />');
assert(xmlDoc.innerHTML, '<android.view.View content-desc="" />');

const xmlNode = xmlDoc.ownerDocument.createElement('div');
xmlNode.innerHTML = '<p>!</p>';
assert(xmlNode.innerHTML, '<p>!</p>', 'innerHTML');
xmlNode.insertAdjacentHTML('beforebegin', 'beforebegin');
xmlNode.insertAdjacentHTML('afterend', 'afterend');
assert(xmlNode.toString(), '<div><p>!</p></div>', 'no element, no before/after');
xmlNode.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin');
assert(xmlNode.toString(), '<div>beforebegin<p>!</p></div>', 'beforebegin works');
xmlNode.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin');
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!</p></div>', 'afterbegin works');
xmlNode.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend');
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p></div>', 'beforeend works');
xmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend');
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>afterend</div>', 'afterend works');

xmlNode.firstElementChild.insertAdjacentHTML('beforeend', '<i>1</i><i>2</i>');
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>afterend</div>', 'multiple html works');

xmlNode.firstElementChild.insertAdjacentText('afterend', '<OK>');
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>&lt;OK&gt;afterend</div>', 'insertAdjacentText works');

const xmlDocWithEmptyAttrFromSet = parser.parseFromString(`<hierarchy><android.view.View style=""/></hierarchy>`, 'text/xml').documentElement;// attribute is in emptyAttributes set is empty (even for XML)
assert(xmlDocWithEmptyAttrFromSet.firstChild.getAttribute('style'), '');
assert(xmlDocWithEmptyAttrFromSet.firstChild.outerHTML, '<android.view.View style="" />');
Expand Down
10 changes: 9 additions & 1 deletion test/interface/range.js
Original file line number Diff line number Diff line change
@@ -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('<html><div class="test">abc</div></html>');

Expand Down Expand Up @@ -70,3 +70,11 @@ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
range.selectNodeContents(svg);
const rect = range.createContextualFragment('<rect />').childNodes[0];
assert('ownerSVGElement' in rect, true, 'createContextualFragment(SVG)');

{
const svgDocument = (new DOMParser).parseFromString('<!doctype svg><svg></svg>', 'image/svg+xml');

let range = svgDocument.createRange();
let contextual = range.createContextualFragment('<div>hi</div>');
assert(contextual.toString(), '<#document-fragment><div>hi</div></#document-fragment>', 'createContextualFragment');
}
2 changes: 1 addition & 1 deletion types/esm/interface/range.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions types/esm/shared/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
32 changes: 26 additions & 6 deletions worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down