Skip to content

Commit c578172

Browse files
authored
Fix insertAdjacentHTML and createContextualFragment not working on non-HTML documents (#307)
* Create helper function `htmlToFragment` * Fix insertAdjacentHTML not working on XML (and potentially SVG) documents * Fix createContextualFragment not working on XML (and potentially SVG) documents * Move insertAdjacent... tests from html to general elements, and cover XML * Add test for createContextualFragment in SVG documents * Build * Improve performance of htmlToFragment, and added JSDoc * Add test to check insertAdjacentHTML adding multiple elements * Build
1 parent b6d1859 commit c578172

File tree

12 files changed

+136
-38
lines changed

12 files changed

+136
-38
lines changed

cjs/interface/element.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
} = require('../shared/symbols.js');
2222

2323
const {
24+
htmlToFragment,
2425
ignoreCase,
2526
knownAdjacent,
2627
localCase,
@@ -382,9 +383,7 @@ class Element extends ParentNode {
382383
}
383384

384385
insertAdjacentHTML(position, html) {
385-
const template = this.ownerDocument.createElement('template');
386-
template.innerHTML = html;
387-
this.insertAdjacentElement(position, template.content);
386+
this.insertAdjacentElement(position, htmlToFragment(this.ownerDocument, html));
388387
}
389388

390389
insertAdjacentText(position, text) {

cjs/interface/range.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const {END, NEXT, PREV, START} = require('../shared/symbols.js');
55

66
const {SVGElement} = require('../svg/element.js');
77

8-
const {getEnd, setAdjacent} = require('../shared/utils.js');
8+
const {getEnd, htmlToFragment, setAdjacent} = require('../shared/utils.js');
99

1010
const deleteContents = ({[START]: start, [END]: end}, fragment = null) => {
1111
setAdjacent(start[PREV], end[NEXT]);
@@ -102,9 +102,7 @@ class Range {
102102
const { commonAncestorContainer: doc } = this;
103103
const isSVG = 'ownerSVGElement' in doc;
104104
const document = isSVG ? doc.ownerDocument : doc;
105-
const template = document.createElement('template');
106-
template.innerHTML = html;
107-
let {content} = template;
105+
let content = htmlToFragment(document, html);
108106
if (isSVG) {
109107
const childNodes = [...content.childNodes];
110108
content = document.createDocumentFragment();

cjs/shared/utils.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,28 @@ const setAdjacent = (prev, next) => {
4747
next[PREV] = prev;
4848
};
4949
exports.setAdjacent = setAdjacent;
50+
51+
/**
52+
* @param {import("../interface/document.js").Document} ownerDocument
53+
* @param {string} html
54+
* @return {import("../interface/document-fragment.js").DocumentFragment}
55+
*/
56+
const htmlToFragment = (ownerDocument, html) => {
57+
const fragment = ownerDocument.createDocumentFragment();
58+
59+
const elem = ownerDocument.createElement('');
60+
elem.innerHTML = html;
61+
const { firstChild, lastChild } = elem;
62+
63+
if (firstChild) {
64+
knownSegment(fragment, firstChild, lastChild, fragment[END]);
65+
66+
let child = firstChild;
67+
do {
68+
child.parentNode = fragment;
69+
} while (child !== lastChild && (child = getEnd(child)[NEXT]));
70+
}
71+
72+
return fragment;
73+
};
74+
exports.htmlToFragment = htmlToFragment;

esm/interface/element.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../shared/symbols.js';
2424

2525
import {
26+
htmlToFragment,
2627
ignoreCase,
2728
knownAdjacent,
2829
localCase,
@@ -384,9 +385,7 @@ export class Element extends ParentNode {
384385
}
385386

386387
insertAdjacentHTML(position, html) {
387-
const template = this.ownerDocument.createElement('template');
388-
template.innerHTML = html;
389-
this.insertAdjacentElement(position, template.content);
388+
this.insertAdjacentElement(position, htmlToFragment(this.ownerDocument, html));
390389
}
391390

392391
insertAdjacentText(position, text) {

esm/interface/range.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {END, NEXT, PREV, START} from '../shared/symbols.js';
44

55
import {SVGElement} from '../svg/element.js';
66

7-
import {getEnd, setAdjacent} from '../shared/utils.js';
7+
import {getEnd, htmlToFragment, setAdjacent} from '../shared/utils.js';
88

99
const deleteContents = ({[START]: start, [END]: end}, fragment = null) => {
1010
setAdjacent(start[PREV], end[NEXT]);
@@ -101,9 +101,7 @@ export class Range {
101101
const { commonAncestorContainer: doc } = this;
102102
const isSVG = 'ownerSVGElement' in doc;
103103
const document = isSVG ? doc.ownerDocument : doc;
104-
const template = document.createElement('template');
105-
template.innerHTML = html;
106-
let {content} = template;
104+
let content = htmlToFragment(document, html);
107105
if (isSVG) {
108106
const childNodes = [...content.childNodes];
109107
content = document.createDocumentFragment();

esm/shared/utils.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,27 @@ export const setAdjacent = (prev, next) => {
3838
if (next)
3939
next[PREV] = prev;
4040
};
41+
42+
/**
43+
* @param {import("../interface/document.js").Document} ownerDocument
44+
* @param {string} html
45+
* @return {import("../interface/document-fragment.js").DocumentFragment}
46+
*/
47+
export const htmlToFragment = (ownerDocument, html) => {
48+
const fragment = ownerDocument.createDocumentFragment();
49+
50+
const elem = ownerDocument.createElement('');
51+
elem.innerHTML = html;
52+
const { firstChild, lastChild } = elem;
53+
54+
if (firstChild) {
55+
knownSegment(fragment, firstChild, lastChild, fragment[END]);
56+
57+
let child = firstChild;
58+
do {
59+
child.parentNode = fragment;
60+
} while (child !== lastChild && (child = getEnd(child)[NEXT]));
61+
}
62+
63+
return fragment;
64+
};

test/html/element.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,22 +113,6 @@ node.nonce = 'abc';
113113
assert(node.nonce, 'abc', 'yes nonce');
114114

115115
node = document.createElement('div');
116-
node.innerHTML = '<p>!</p>';
117-
assert(node.innerHTML, '<p>!</p>', 'innerHTML');
118-
node.insertAdjacentHTML('beforebegin', 'beforebegin');
119-
node.insertAdjacentHTML('afterend', 'afterend');
120-
assert(node.toString(), '<div><p>!</p></div>', 'no element, no before/after');
121-
node.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin');
122-
assert(node.toString(), '<div>beforebegin<p>!</p></div>', 'beforebegin works');
123-
node.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin');
124-
assert(node.toString(), '<div>beforebegin<p>afterbegin!</p></div>', 'afterbegin works');
125-
node.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend');
126-
assert(node.toString(), '<div>beforebegin<p>afterbegin!beforeend</p></div>', 'beforeend works');
127-
node.firstElementChild.insertAdjacentHTML('afterend', 'afterend');
128-
assert(node.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>afterend</div>', 'afterend works');
129-
130-
node.firstElementChild.insertAdjacentText('afterend', '<OK>');
131-
assert(node.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>&lt;OK&gt;afterend</div>', 'insertAdjacentText works');
132116

133117
node.setAttribute('a', '1');
134118
assert(node.attributes[0].name, 'a')

test/interface/element.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ assert(htmlDoc.firstChild.getAttribute('content-desc'), '');
3131
assert(htmlDoc.firstChild.outerHTML, '<span content-desc=""></span>');
3232
assert(htmlDoc.innerHTML, '<span content-desc=""></span>');
3333

34+
const htmlNode = htmlDoc.ownerDocument.createElement('div');
35+
htmlNode.innerHTML = '<p>!</p>';
36+
assert(htmlNode.innerHTML, '<p>!</p>', 'innerHTML');
37+
htmlNode.insertAdjacentHTML('beforebegin', 'beforebegin');
38+
htmlNode.insertAdjacentHTML('afterend', 'afterend');
39+
assert(htmlNode.toString(), '<div><p>!</p></div>', 'no element, no before/after');
40+
htmlNode.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin');
41+
assert(htmlNode.toString(), '<div>beforebegin<p>!</p></div>', 'beforebegin works');
42+
htmlNode.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin');
43+
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!</p></div>', 'afterbegin works');
44+
htmlNode.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend');
45+
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p></div>', 'beforeend works');
46+
htmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend');
47+
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>afterend</div>', 'afterend works');
48+
49+
htmlNode.firstElementChild.insertAdjacentHTML('beforeend', '<i>1</i><i>2</i>');
50+
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>afterend</div>', 'multiple html works');
51+
52+
htmlNode.firstElementChild.insertAdjacentText('afterend', '<OK>');
53+
assert(htmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>&lt;OK&gt;afterend</div>', 'insertAdjacentText works');
54+
3455
const htmlDocWithEmptyAttrFromSet = parser.parseFromString(`<div><span style=""/></div>`, 'text/html').documentElement; // attribute is in emptyAttributes set is empty
3556

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

72+
const xmlNode = xmlDoc.ownerDocument.createElement('div');
73+
xmlNode.innerHTML = '<p>!</p>';
74+
assert(xmlNode.innerHTML, '<p>!</p>', 'innerHTML');
75+
xmlNode.insertAdjacentHTML('beforebegin', 'beforebegin');
76+
xmlNode.insertAdjacentHTML('afterend', 'afterend');
77+
assert(xmlNode.toString(), '<div><p>!</p></div>', 'no element, no before/after');
78+
xmlNode.firstElementChild.insertAdjacentHTML('beforebegin', 'beforebegin');
79+
assert(xmlNode.toString(), '<div>beforebegin<p>!</p></div>', 'beforebegin works');
80+
xmlNode.firstElementChild.insertAdjacentHTML('afterbegin', 'afterbegin');
81+
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!</p></div>', 'afterbegin works');
82+
xmlNode.firstElementChild.insertAdjacentHTML('beforeend', 'beforeend');
83+
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p></div>', 'beforeend works');
84+
xmlNode.firstElementChild.insertAdjacentHTML('afterend', 'afterend');
85+
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend</p>afterend</div>', 'afterend works');
86+
87+
xmlNode.firstElementChild.insertAdjacentHTML('beforeend', '<i>1</i><i>2</i>');
88+
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>afterend</div>', 'multiple html works');
89+
90+
xmlNode.firstElementChild.insertAdjacentText('afterend', '<OK>');
91+
assert(xmlNode.toString(), '<div>beforebegin<p>afterbegin!beforeend<i>1</i><i>2</i></p>&lt;OK&gt;afterend</div>', 'insertAdjacentText works');
92+
5193
const xmlDocWithEmptyAttrFromSet = parser.parseFromString(`<hierarchy><android.view.View style=""/></hierarchy>`, 'text/xml').documentElement;// attribute is in emptyAttributes set is empty (even for XML)
5294
assert(xmlDocWithEmptyAttrFromSet.firstChild.getAttribute('style'), '');
5395
assert(xmlDocWithEmptyAttrFromSet.firstChild.outerHTML, '<android.view.View style="" />');

test/interface/range.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const assert = require('../assert.js').for('Range');
22

3-
const {parseHTML} = global[Symbol.for('linkedom')];
3+
const {parseHTML, DOMParser} = global[Symbol.for('linkedom')];
44

55
const {document} = parseHTML('<html><div class="test">abc</div></html>');
66

@@ -70,3 +70,11 @@ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
7070
range.selectNodeContents(svg);
7171
const rect = range.createContextualFragment('<rect />').childNodes[0];
7272
assert('ownerSVGElement' in rect, true, 'createContextualFragment(SVG)');
73+
74+
{
75+
const svgDocument = (new DOMParser).parseFromString('<!doctype svg><svg></svg>', 'image/svg+xml');
76+
77+
let range = svgDocument.createRange();
78+
let contextual = range.createContextualFragment('<div>hi</div>');
79+
assert(contextual.toString(), '<#document-fragment><div>hi</div></#document-fragment>', 'createContextualFragment');
80+
}

types/esm/interface/range.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class Range implements globalThis.Range {
1414
cloneContents(): any;
1515
deleteContents(): void;
1616
extractContents(): any;
17-
createContextualFragment(html: any): any;
17+
createContextualFragment(html: any): import("./document-fragment.js").DocumentFragment;
1818
cloneRange(): Range;
1919
[START]: any;
2020
[END]: any;

0 commit comments

Comments
 (0)