Skip to content

Commit

Permalink
feat: setting HTMLElement.outerHTML (closes #150)
Browse files Browse the repository at this point in the history
  • Loading branch information
b-fuze committed Nov 10, 2023
1 parent da161cf commit a4e2812
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 3 deletions.
36 changes: 35 additions & 1 deletion src/dom/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,41 @@ export class Element extends Node {
}

set outerHTML(html: string) {
// TODO: Someday...
if (this.parentNode) {
const { parentElement, parentNode } = this;
let contextLocalName = parentElement?.localName;

switch (parentNode.nodeType) {
case NodeType.DOCUMENT_NODE: {
throw new DOMException(
"Modifications are not allowed for this document",
);
}

// setting outerHTML, step 4. Document Fragment
// ref: https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml
case NodeType.DOCUMENT_FRAGMENT_NODE: {
contextLocalName = "body";
// fall-through
}

default: {
const { childNodes: newChildNodes } =
fragmentNodesFromString(html, contextLocalName!).childNodes[0];
const mutator = parentNode._getChildNodesMutator();
const insertionIndex = mutator.indexOf(this);

for (let i = newChildNodes.length - 1; i >= 0; i--) {
const child = newChildNodes[i];
mutator.splice(insertionIndex, 0, child);
child._setParent(parentNode);
child._setOwnerDocument(parentNode.ownerDocument);
}

this.remove();
}
}
}
}

get innerHTML(): string {
Expand Down
111 changes: 109 additions & 2 deletions test/units/Element-outerHTML.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { DOMParser } from "../../deno-dom-wasm.ts";
import { assertStrictEquals as assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
import { DocumentFragment, DOMParser } from "../../deno-dom-wasm.ts";
import {
assertStrictEquals as assertEquals,
assertThrows,
} from "https://deno.land/[email protected]/testing/asserts.ts";

// TODO: More comprehensive tests

Expand Down Expand Up @@ -69,3 +72,107 @@ Deno.test("Element.outerHTML won't overflow the stack for deeply nested HTML", (
const htmlElement = doc.documentElement!;
assertEquals(htmlElement.outerHTML.length > 0, true);
});

Deno.test("Element.outerHTML can be set to replace element", () => {
const doc = new DOMParser().parseFromString(
`
<div class=parent><div class=child></div></div>
<div class=otherparent><span>1st</span><div class=otherchild>second</div><!--third--></div>
<table><tbody><tr><td>hello</td></tr></tbody></table>
`,
"text/html",
)!;
const parent = doc.querySelector(".parent")!;
const child = doc.querySelector(".child")!;
const otherParent = doc.querySelector(".otherparent")!;
const otherChild = doc.querySelector(".otherchild")!;
const tbody = doc.querySelector("tbody")!;
const tr = tbody.children[0];

const newHTML =
`<span>foo <em>fib</em></span>text nodes<strong fizz=qux>bar</strong><!--comment-->`;
const serializedNewHTML = newHTML.replace("qux", '"qux"');
child.outerHTML = newHTML;

assertEquals(child.parentNode, null);
assertEquals(
Array.from(parent.childNodes).find((node) => node === child),
undefined,
);
assertEquals(parent.children.length, 2);
assertEquals(parent.childNodes.length, 4);
assertEquals(parent.innerHTML, serializedNewHTML);

const newChild = parent.children[0];
newChild.outerHTML = `<tr><td>goodbye</td></tr>`;
assertEquals(newChild.parentNode, null);
assertEquals(
Array.from(parent.childNodes).find((node) => node === newChild),
undefined,
);
assertEquals(parent.children.length, 1);
assertEquals(parent.childNodes.length, 4);
assertEquals(
parent.innerHTML,
"goodbye" + serializedNewHTML.slice(serializedNewHTML.indexOf("text")),
);

assertEquals(tbody.innerHTML, `<tr><td>hello</td></tr>`);
tr.outerHTML = `<tr><td>goodbye</td></tr>`;

assertEquals(tr.parentNode, null);
assertEquals(
Array.from(tbody.childNodes).find((node) => node === tr),
undefined,
);
assertEquals(tbody.children.length, 1);
assertEquals(tbody.childNodes.length, 1);
assertEquals(tbody.innerHTML, `<tr><td>goodbye</td></tr>`);

otherChild.outerHTML = `<aside>seconded</aside><!--not counted-->`;

assertEquals(otherChild.parentNode, null);
assertEquals(otherChild.parentElement, null);
assertEquals(
Array.from(otherParent.childNodes).find((node) => node === otherChild),
undefined,
);
assertEquals(otherParent.children.length, 2);
assertEquals(otherParent.childNodes.length, 4);
assertEquals(
otherParent.outerHTML,
`<div class="otherparent"><span>1st</span><aside>seconded</aside><!--not counted--><!--third--></div>`,
);

const solitaryDiv = doc.createElement("div");
solitaryDiv.outerHTML = `<span>no-op</span>`;

assertEquals(solitaryDiv.parentNode, null);
assertEquals(solitaryDiv.outerHTML, `<div></div>`);

const frag = doc.createDocumentFragment();
const fragChild = doc.createElement("div");
frag.appendChild(fragChild);
assertEquals(fragChild.parentNode, frag);
assertEquals(frag.childNodes[0].nodeName, "DIV");
assertEquals(frag.childNodes.length, 1);

fragChild.outerHTML = `
<aside></aside>
<!---->
<tr><td>only text nodes allowed</td></tr>
<button></button>
`.replace(/\s{2,}/g, "");

assertEquals(fragChild.parentNode, null);
assertEquals(frag.children.length, 2);
assertEquals(frag.childNodes.length, 4);
assertEquals(
Array.from(frag.childNodes).map((node) => node.nodeName).join("-"),
"ASIDE-#comment-#text-BUTTON",
);

assertThrows(() => {
doc.documentElement!.outerHTML = "<div>not new document element</div>";
});
});

0 comments on commit a4e2812

Please sign in to comment.