Skip to content

Commit

Permalink
Implement changes from whatwg/dom#819
Browse files Browse the repository at this point in the history
The DOM spec is currently broken because of changes around the adoption of
nodes. There is an open PR which reverts some of these changes. This
implements the same changes as in that PR to fix issues where some mutations
would otherwise generate invalid mutation records.
  • Loading branch information
bwrrp committed Dec 20, 2021
1 parent b06bb4c commit 71b0af0
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 54 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ This library is currently aimed at providing a lightweight and consistent experi

Do not rely on the behavior or presence of any methods and properties not specified in the DOM standard. For example, do not use JavaScript array methods exposed on properties that should expose a NodeList and do not use Element as a constructor. This behavior is _not_ considered public API and may change without warning in a future release.

This library implements the changes from [whatwg/dom#819][dom-adopt-pr], as the specification as currently described has known bugs around adoption.

### Parsing

This library does not implement the `DOMParser` interface, nor `insertAdjacentHTML` on `Element`, nor `createContextualFragment` on `Range`. The `innerHTML` and `outerHTML` properties are read-only,
Expand All @@ -80,7 +82,7 @@ Emulating a full browser environment is not the goal of this library. Consider u

This implementation offers no special treatment of HTML documents, which means there are no implementations of `HTMLElement` and its subclasses. This also affects HTML-specific casing behavior for attributes and tagNames. The `id` / `className` / `classList` properties on `Element` and `compatMode` / `contentType` on `Document` have not been implemented. HTML-specific query methods (`getElementById` for interface `NonElementParentNode`, `getElementsByClassName` on `Document`) are also missing.

This library also does not currently implement events, including the `Event` / `EventTarget` interfaces. It also currently does not contain an implementation of `AbortController` / `AbortSignal`. As these may have wider applications than browser-specific use cases, please file an issue if you have a use for these in your application and would like support for them to be added.
This library does not currently implement events, including the `Event` / `EventTarget` interfaces. It also currently does not contain an implementation of `AbortController` / `AbortSignal`. As these may have wider applications than browser-specific use cases, please file an issue if you have a use for these in your application and would like support for them to be added.

There is currently no support for shadow DOM, so no `Slottable` / `ShadowRoot` interfaces and no `slot` / `attachShadow` / `shadowRoot` on `Element`. Slimdom also does not support the APIs for custom elements using the `is` option on `createElement` / `createElementNS`.

Expand All @@ -97,6 +99,7 @@ The following features are missing simply because I have not yet had a need for
- `attributeFilter` for mutation observers.
- `isConnected` / `getRootNode` / `isEqualNode` / `isSameNode` / `compareDocumentPosition` on `Node`

[dom-adopt-pr]: https://github.com/whatwg/dom/pull/819
[slimdom-sax-parser]: https://github.com/wvbe/slimdom-sax-parser
[fontoxpath]: https://github.com/FontoXML/fontoxpath/
[parse5-example]: https://github.com/bwrrp/slimdom.js/tree/main/test/examples/parse5
Expand Down
5 changes: 3 additions & 2 deletions src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,9 @@ export default class Document extends Node implements NonElementParentNode, Pare
}

// 2. If node is a shadow root, then throw a HierarchyRequestError.
// 3. If node is a DocumentFragment whose host is non-null, then return.
// (shadow dom not implemented)
// 3. If node is a DocumentFragment node and its host is non-null, then return node.
// Note: unfortunately this does not throw for web compatibility.
// (shadow dom and HTML templates not implemented)

// 4. Adopt node into this.
adoptNode(node, this);
Expand Down
107 changes: 56 additions & 51 deletions src/util/mutationAlgorithms.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { throwHierarchyRequestError, throwNotFoundError } from './errorHelpers';
import { NodeType, isNodeOfType } from './NodeType';
import {
determineLengthOfNode,
getNodeDocument,
getNodeIndex,
forEachInclusiveDescendant,
} from './treeHelpers';
import { getNodeDocument, getNodeIndex, forEachInclusiveDescendant } from './treeHelpers';
import { insertIntoChildren, removeFromChildren } from './treeMutations';
import Document from '../Document';
import DocumentFragment from '../DocumentFragment';
Expand Down Expand Up @@ -179,10 +174,13 @@ export function preInsertNode<TNode extends Node>(
referenceChild = node.nextSibling;
}

// 4. Insert node into parent before referenceChild.
// 4. Adopt node into parent's node document.
adoptNode(node, getNodeDocument(parent));

// 5. Insert node into parent before referenceChild.
insertNode(node, parent, referenceChild);

// 5. Return node.
// 6. Return node.
return node;
}

Expand Down Expand Up @@ -247,40 +245,30 @@ export function insertNode(
// 6. Let previousSibling be child’s previous sibling or parent’s last child if child is null.
let previousSibling = child === null ? parent.lastChild : child.previousSibling;

// Non-standard: it appears the standard as of 27 January 2021 does not account for
// previousSibling now possibly being node, which can happen, for instance, when doing
// parent.insertBefore(child, child);
if (previousSibling === node) {
previousSibling = node.previousSibling;
}

// 7. For each node in nodes, in tree order:
nodes.forEach((node) => {
// 7.1. Adopt node into parent's node document.
adoptNode(node, getNodeDocument(parent));

// 7.2. If child is null, then append node to parent’s children.
// 7.3. Otherwise, insert node into parent’s children before child’s index.
// 7.1. If child is null, then append node to parent’s children.
// 7.2. Otherwise, insert node into parent’s children before child’s index.
insertIntoChildren(node, parent, child);

// 7.4. If parent is a shadow host and node is a slottable, then assign a slot for node.
// 7.3. If parent is a shadow host and node is a slottable, then assign a slot for node.
// (shadow dom not implemented)

// 7.5. If parent's root is a shadow root, and parent is a slot whose assigned nodes is the
// 7.4. If parent's root is a shadow root, and parent is a slot whose assigned nodes is the
// empty list, then run signal a slot change for parent.
// 7.6. Run assign slottables for a tree with node’s tree.
// 7.5. Run assign slottables for a tree with node’s tree.
// (shadow dom not implemented)

// 7.7. For each shadow-including inclusive descendant inclusiveDescendant of node, in
// 7.6. For each shadow-including inclusive descendant inclusiveDescendant of node, in
// shadow-including tree order:
// 7.7.1. Run the insertion steps with inclusiveDescendant.
// 7.6.1. Run the insertion steps with inclusiveDescendant.
// (insertion steps not implemented)

// 7.7.2. If inclusiveDescendant is connected, then:
// 7.7.2.1. If inclusiveDescendant is custom, then enqueue a custom element callback
// 7.6.2. If inclusiveDescendant is connected, then:
// 7.6.2.1. If inclusiveDescendant is custom, then enqueue a custom element callback
// reaction with inclusiveDescendant, callback name "connectedCallback", and an empty
// argument list.
// 7.7.2.2. Otherwise, try to upgrade inclusiveDescendant. If this successfully upgrades
// 7.6.2.2. Otherwise, try to upgrade inclusiveDescendant. If this successfully upgrades
// inclusiveDescendant, its connectedCallback will be enqueued automatically during the
// upgrade an element algorithm.
// (custom elements not implemented)
Expand Down Expand Up @@ -465,11 +453,13 @@ export function replaceChildWithNode<TChild extends Node>(
// 9. Let previousSibling be child’s previous sibling.
const previousSibling = child.previousSibling;

// 10. Let removedNodes be the empty set.
// 10. Adopt node into parent's node document
adoptNode(node, getNodeDocument(parent));

// 11. Let removedNodes be the empty set.
let removedNodes: Node[] = [];

// 11. If child’s parent is non-null, then:
/* istanbul ignore else */
// 12. If child’s parent is non-null, then:
if (child.parentNode !== null) {
// 11.1. Set removedNodes to « child ».
removedNodes.push(child);
Expand All @@ -478,17 +468,16 @@ export function replaceChildWithNode<TChild extends Node>(
removeNode(child, true);
}
// The above can only be false if child is node.
// (TODO: this is no longer the case, at least until whatwg/dom#819 is merged)

// 12. Let nodes be node’s children if node is a DocumentFragment node; otherwise « node ».
// 13. Let nodes be node’s children if node is a DocumentFragment node; otherwise « node ».
const nodes = isNodeOfType(node, NodeType.DOCUMENT_FRAGMENT_NODE)
? Array.from(node.childNodes)
: [node];

// 13. Insert node into parent before referenceChild with the suppress observers flag set.
// 14. Insert node into parent before referenceChild with the suppress observers flag set.
insertNode(node, parent, referenceChild, true);

// 14. Queue a tree mutation record for parent with nodes, removedNodes, previousSibling and
// 15. Queue a tree mutation record for parent with nodes, removedNodes, previousSibling and
// referenceChild.
queueMutationRecord('childList', parent, {
addedNodes: nodes,
Expand All @@ -497,7 +486,7 @@ export function replaceChildWithNode<TChild extends Node>(
previousSibling: previousSibling,
});

// 15. Return child.
// 16. Return child.
return child;
}

Expand All @@ -508,36 +497,41 @@ export function replaceChildWithNode<TChild extends Node>(
* @param parent Parent to replace under
*/
function replaceAllWithNode(node: Node | null, parent: Node): void {
// 1. Let removedNodes be parent’s children.
// 1. If node is non-null, then adopt node into parent's node document
if (node !== null) {
adoptNode(node, getNodeDocument(parent));
}

// 2. Let removedNodes be parent’s children.
const removedNodes = Array.from(parent.childNodes);

// 2. Let addedNodes be the empty set.
// 3. Let addedNodes be the empty set.
let addedNodes: Node[] = [];

if (node !== null) {
// 3. If node is a DocumentFragment node, then set addedNodes to node's children.
// 4. If node is a DocumentFragment node, then set addedNodes to node's children.
if (isNodeOfType(node, NodeType.DOCUMENT_FRAGMENT_NODE)) {
node.childNodes.forEach((child) => {
addedNodes.push(child);
});
} else {
// 4. Otherwise, if node is non-null, set addedNodes to « node ».
// 5. Otherwise, if node is non-null, set addedNodes to « node ».
addedNodes.push(node);
}
}

// 5. Remove all parent’s children, in tree order, with the suppress observers flag set.
// 6. Remove all parent’s children, in tree order, with the suppress observers flag set.
removedNodes.forEach((child) => {
removeNode(child, true);
});

// 6. If node is non-null, then insert node into parent before null with the suppress observers
// 7. If node is non-null, then insert node into parent before null with the suppress observers
// flag set.
if (node !== null) {
insertNode(node, parent, null, true);
}

// 7. If either addedNodes or removedNodes is not empty, then queue a tree mutation record for
// 8. If either addedNodes or removedNodes is not empty, then queue a tree mutation record for
// parent with addedNodes, removedNodes, null, and null.
if (addedNodes.length > 0 || removedNodes.length > 0) {
queueMutationRecord('childList', parent, {
Expand Down Expand Up @@ -688,13 +682,18 @@ export function removeNode(node: Node, suppressObservers: boolean = false): void
/**
* 3.5. Interface Document
*
* To adopt a node into a document, run these steps:
* To adopt a node into a document, with an optional forceDocumentFragmentAdoption, run these steps:
*
* (forceDocumentFragmentAdoption is only set to true for HTML template, so is not implemented here)
*
* @param node - Node to adopt
* @param document - Document to adopt node into
*/
export function adoptNode(node: Node, document: Document): void {
// 1. Let oldDocument be node’s node document.
// 1. If forceDocumentFragmentAdoption is not given, then set it false.
// (value unused)

// 2. Let oldDocument be node’s node document.
const oldDocument = getNodeDocument(node);

// 2. If node’s parent is non-null, remove node.
Expand All @@ -708,15 +707,21 @@ export function adoptNode(node: Node, document: Document): void {
}

// 3.1. For each inclusiveDescendant in node’s shadow-including inclusive descendants:
forEachInclusiveDescendant(node, (node) => {
// 3.1.1. Set inclusiveDescendant’s node document to document.
forEachInclusiveDescendant(node, (inclusiveDescendant) => {
// 3.1.1. If forceDocumentFragmentAdoption is false, inclusiveDescendant is a
// DocumentFragment node, inclusiveDescendant is node, and node's host is non-null, then
// continue
// Note: this is only reasonable as long as all adopt callers remove the children of node.
// (shadow dom and HTML templates not implemented)

// 3.1.2. Set inclusiveDescendant’s node document to document.
// (calling code ensures that node is never a Document)
node.ownerDocument = document;
inclusiveDescendant.ownerDocument = document;

// 3.1.2. If inclusiveDescendant is an element, then set the node document of each attribute
// 3.1.3. If inclusiveDescendant is an element, then set the node document of each attribute
// in inclusiveDescendant’s attribute list to document.
if (isNodeOfType(node, NodeType.ELEMENT_NODE)) {
for (const attr of (node as Element).attributes) {
if (isNodeOfType(inclusiveDescendant, NodeType.ELEMENT_NODE)) {
for (const attr of (inclusiveDescendant as Element).attributes) {
attr.ownerDocument = document;
}
}
Expand Down

0 comments on commit 71b0af0

Please sign in to comment.