jsdom
Version:
A JavaScript implementation of many web standards
367 lines (340 loc) • 11.4 kB
JavaScript
"use strict";
// Portions adapted from SymbolTree (https://github.com/jsdom/js-symbol-tree).
// Copyright (c) 2015 Joris van der Wel
// Licensed under the MIT license; see LICENSE.txt.
const NODE_DOCUMENT_POSITION = require("../node-document-position");
// DOM links are allocated only when a node joins a tree or gains children. In
// particular, reading the tree must not allocate storage for Attr or new nodes.
// Insertion callers provide detached children. Mutations here only change links:
// NodeImpl handles validation, adoption, ranges, observers, slots, and reactions.
class Links {
parent = null;
previousSibling = null;
nextSibling = null;
firstChild = null;
lastChild = null;
childCount = 0;
// null: append-time indexes are valid; undefined: they need rebuilding;
// array: a lazily indexed prefix of the children.
cachedChildren = null;
cachedIndex = -1;
}
function ensureLinks(node) {
const data = node._links;
if (data) {
return data;
}
node._links = new Links();
return node._links;
}
function lastInclusiveDescendant(node) {
let child;
while ((child = node.lastChild)) {
node = child;
}
return node;
}
function nextAfterSubtree(node, root) {
while (node !== root && node !== null) {
const data = node._links;
if (data === null) {
return null;
}
if (data.nextSibling) {
return data.nextSibling;
}
node = data.parent;
}
return null;
}
function nextInTree(node, root) {
const data = node._links;
return data === null ? null : data.firstChild || nextAfterSubtree(node, root);
}
function previousInTree(node, root) {
if (node === root) {
return null;
}
const data = node._links;
if (data === null) {
return null;
}
return data.previousSibling ? lastInclusiveDescendant(data.previousSibling) : data.parent;
}
// Iterators capture the next light-tree node before yielding the current one.
// This matters when a consumer removes or moves the current/next node.
class ChildrenIterator extends Iterator {
constructor(next) {
super();
this.current = next;
}
next() {
const value = this.current;
if (value === null) {
return { value: undefined, done: true };
}
this.current = value.nextSibling;
return { value, done: false };
}
}
class DescendantsIterator extends Iterator {
constructor(root, next) {
super();
this.root = root;
this.current = next;
}
next() {
const value = this.current;
if (value === null) {
return { value: undefined, done: true };
}
this.current = nextInTree(value, this.root);
return { value, done: false };
}
}
// https://dom.spec.whatwg.org/#concept-shadow-including-descendant
class ShadowIncludingIterator extends DescendantsIterator {
constructor(root) {
super(root, root);
this.previous = null;
this.stack = null;
}
next() {
// A host's shadow root is inspected after visiting the host. Its light-tree
// successor was captured before visiting it. Keep both mutation semantics.
if (this.previous && this.previous._shadowRoot) {
this.stack ||= [];
this.stack.push(this.root, this.current);
this.root = this.previous._shadowRoot;
this.current = this.root;
}
while (this.current === null && this.stack && this.stack.length > 0) {
this.current = this.stack.pop();
this.root = this.stack.pop();
}
const value = this.current;
this.previous = value;
if (value === null) {
return { value: undefined, done: true };
}
this.current = nextInTree(value, this.root);
return { value, done: false };
}
}
// Collection refreshes avoid iterator-result objects and intermediate filter iterators.
function childrenToArray(node, filter) {
const array = [];
for (let child = node.firstChild; child !== null; child = child.nextSibling) {
if (filter === undefined || filter(child)) {
array.push(child);
}
}
return array;
}
function descendantsToArray(root, node, filter) {
const array = [];
while (node !== null) {
if (filter === undefined || filter(node)) {
array.push(node);
}
node = nextInTree(node, root);
}
return array;
}
function compareTreePosition(left, right) {
if (left === right) {
return 0;
}
// NodeImpl represents an unattached Attr by its null owner element.
if (left === null || right === null) {
return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED;
}
const leftParent = left.parentNode;
const rightParent = right.parentNode;
if (leftParent === rightParent) {
if (leftParent === null) {
return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED;
}
return left._treeIndex() < right._treeIndex() ?
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_FOLLOWING :
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_PRECEDING;
}
// When one node is a root, only the other node's root is needed. Avoid
// counting depths and comparing ancestors on every step of a deep chain.
if (leftParent === null) {
return rightParent.getRootNode() === left ?
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_CONTAINED_BY | NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_FOLLOWING :
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED;
}
if (rightParent === null) {
return leftParent.getRootNode() === right ?
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_CONTAINS | NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_PRECEDING :
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED;
}
const { ancestor, leftChild, rightChild } = commonAncestorInfo(left, right);
if (ancestor === null) {
return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED;
}
if (ancestor === left) {
return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_CONTAINED_BY | NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_FOLLOWING;
}
if (ancestor === right) {
return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_CONTAINS | NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_PRECEDING;
}
return leftChild._treeIndex() < rightChild._treeIndex() ?
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_FOLLOWING :
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_PRECEDING;
}
// Return the common ancestor and its child on each path. A child is null when that endpoint is the ancestor itself.
function commonAncestorInfo(left, right) {
if (left === right) {
return { ancestor: left, leftChild: null, rightChild: null };
}
if (left.parentNode === right.parentNode) {
return { ancestor: left.parentNode, leftChild: left, rightChild: right };
}
// Walk together so equally deep nodes can stop at their common ancestor.
// Keep the paths for unequal depths instead of walking the DOM links twice.
const leftAncestors = [];
const rightAncestors = [];
let a = left;
let b = right;
while (a !== null || b !== null) {
if (a === right) {
return { ancestor: right, leftChild: leftAncestors.at(-1), rightChild: null };
}
if (b === left) {
return { ancestor: left, leftChild: null, rightChild: rightAncestors.at(-1) };
}
if (a === b) {
return { ancestor: a, leftChild: leftAncestors.at(-1), rightChild: rightAncestors.at(-1) };
}
if (a !== null) {
leftAncestors.push(a);
a = a.parentNode;
}
if (b !== null) {
rightAncestors.push(b);
b = b.parentNode;
}
}
let i = leftAncestors.length - 1;
let j = rightAncestors.length - 1;
if (leftAncestors[i] !== rightAncestors[j]) {
return { ancestor: null, leftChild: null, rightChild: null };
}
// Neither node is an ancestor of the other, so the paths diverge before
// either array is exhausted.
while (leftAncestors[i] === rightAncestors[j]) {
--i;
--j;
}
return { ancestor: leftAncestors[i + 1], leftChild: leftAncestors[i], rightChild: rightAncestors[j] };
}
function treeIndex(node) {
const data = node._links;
if (data === null || data.parent === null) {
return -1;
}
const parentData = data.parent._links;
if (data.previousSibling === null) {
return 0;
}
if (data.nextSibling === null) {
return parentData.childCount - 1;
}
if (parentData.cachedChildren === null) {
return data.cachedIndex;
}
const cached = parentData.cachedChildren ||= [];
// A shifted node can retain an old index inside the rebuilt prefix. Check
// identity as well as the index before using it.
if (cached[data.cachedIndex] === node) {
return data.cachedIndex;
}
let child = cached.length === 0 ? parentData.firstChild : cached.at(-1).nextSibling;
while (child !== node) {
child._links.cachedIndex = cached.length;
cached.push(child);
child = child.nextSibling;
}
data.cachedIndex = cached.length;
cached.push(node);
return data.cachedIndex;
}
function appendChild(node, child) {
const data = ensureLinks(node);
const childData = ensureLinks(child);
childData.parent = node;
childData.previousSibling = data.lastChild;
if (data.lastChild) {
data.lastChild._links.nextSibling = child;
} else {
data.firstChild = child;
}
data.lastChild = child;
// Appending preserves every existing child's index.
childData.cachedIndex = data.childCount++;
return child;
}
function insertBefore(reference, child) {
const referenceData = reference._links;
const data = referenceData.parent._links;
const childData = ensureLinks(child);
childData.parent = referenceData.parent;
childData.previousSibling = referenceData.previousSibling;
childData.nextSibling = reference;
if (referenceData.previousSibling) {
referenceData.previousSibling._links.nextSibling = child;
} else {
data.firstChild = child;
}
referenceData.previousSibling = child;
++data.childCount;
const cached = data.cachedChildren;
if (cached === null) {
data.cachedChildren = undefined;
} else if (cached && cached[referenceData.cachedIndex] === reference) {
// Only the inserted child and following siblings need new indexes.
cached.length = referenceData.cachedIndex;
}
return child;
}
function remove(node) {
const data = node._links;
if (data === null || data.parent === null) {
return node;
}
const parentData = data.parent._links;
if (data.previousSibling) {
data.previousSibling._links.nextSibling = data.nextSibling;
} else {
parentData.firstChild = data.nextSibling;
}
if (data.nextSibling) {
data.nextSibling._links.previousSibling = data.previousSibling;
} else {
// Removing the last child preserves the remaining indexes.
parentData.lastChild = data.previousSibling;
}
const cached = parentData.cachedChildren;
if (data.nextSibling !== null && cached === null) {
parentData.cachedChildren = undefined;
} else if (cached && cached[data.cachedIndex] === node) {
// Truncating also releases cached references to the removed subtree.
cached.length = data.cachedIndex;
}
--parentData.childCount;
data.parent = null;
data.previousSibling = null;
data.nextSibling = null;
data.cachedIndex = -1;
return node;
}
module.exports = {
ChildrenIterator, DescendantsIterator, ShadowIncludingIterator,
childrenToArray, descendantsToArray,
nextInTree, nextAfterSubtree, previousInTree, lastInclusiveDescendant,
compareTreePosition, commonAncestorInfo,
treeIndex, appendChild, insertBefore, remove
};