Skip to content
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