jsdom
Version:
A JavaScript implementation of many web standards
332 lines (274 loc) • 9.51 kB
JavaScript
"use strict";
const { appendAttribute } = require("./attributes");
const NODE_TYPE = require("./node-type");
const orderedSetParse = require("./helpers/ordered-set").parse;
const { createElement } = require("./helpers/create-element");
const { HTML_NS, XMLNS_NS } = require("./helpers/namespaces");
const { cloningSteps, domSymbolTree } = require("./helpers/internal-constants");
const { asciiCaseInsensitiveMatch, asciiLowercase } = require("./helpers/strings");
const HTMLCollection = require("./generated/HTMLCollection");
exports.clone = (node, document, cloneChildren) => {
if (document === undefined) {
document = node._ownerDocument;
}
let copy;
switch (node.nodeType) {
case NODE_TYPE.DOCUMENT_NODE:
// Can't use a simple `Document.createImpl` because of circular dependency issues :-/
copy = node._cloneDocument();
break;
case NODE_TYPE.DOCUMENT_TYPE_NODE:
copy = document.implementation.createDocumentType(node.name, node.publicId, node.systemId);
break;
case NODE_TYPE.ELEMENT_NODE:
copy = createElement(
document,
node._localName,
node._namespaceURI,
node._prefix,
node._isValue,
false
);
for (const attribute of node._attributeList) {
appendAttribute(copy, exports.clone(attribute, document));
}
break;
case NODE_TYPE.ATTRIBUTE_NODE:
copy = document._createAttribute({
namespace: node._namespace,
namespacePrefix: node._namespacePrefix,
localName: node._localName,
value: node._value
});
break;
case NODE_TYPE.TEXT_NODE:
copy = document.createTextNode(node._data);
break;
case NODE_TYPE.CDATA_SECTION_NODE:
copy = document.createCDATASection(node._data);
break;
case NODE_TYPE.COMMENT_NODE:
copy = document.createComment(node._data);
break;
case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
copy = document.createProcessingInstruction(node.target, node._data);
break;
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
copy = document.createDocumentFragment();
break;
}
if (node[cloningSteps]) {
node[cloningSteps](copy, node, document, cloneChildren);
}
if (cloneChildren) {
for (const child of domSymbolTree.childrenIterator(node)) {
const childCopy = exports.clone(child, document, true);
copy._append(childCopy);
}
}
return copy;
};
// For the following, memoization is not applied here since the memoized results are stored on `this`.
exports.listOfElementsWithClassNames = (classNames, root) => {
// https://dom.spec.whatwg.org/#concept-getElementsByClassName
const classes = orderedSetParse(classNames);
if (classes.size === 0) {
return HTMLCollection.createImpl(root._globalObject, [], { element: root, query: () => [] });
}
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => {
const isQuirksMode = root._ownerDocument.compatMode === "BackCompat";
return domSymbolTree.treeToArray(root, { filter(node) {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
return false;
}
const { classList } = node;
if (isQuirksMode) {
for (const className of classes) {
if (!classList.tokenSet.some(cur => asciiCaseInsensitiveMatch(cur, className))) {
return false;
}
}
} else {
for (const className of classes) {
if (!classList.tokenSet.contains(className)) {
return false;
}
}
}
return true;
} });
}
});
};
exports.listOfElementsWithQualifiedName = (qualifiedName, root) => {
// https://dom.spec.whatwg.org/#concept-getelementsbytagname
if (qualifiedName === "*") {
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => domSymbolTree.treeToArray(root, {
filter: node => node.nodeType === NODE_TYPE.ELEMENT_NODE && node !== root
})
});
}
if (root._ownerDocument._parsingMode === "html") {
const lowerQualifiedName = asciiLowercase(qualifiedName);
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => domSymbolTree.treeToArray(root, {
filter(node) {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
return false;
}
if (node._namespaceURI === HTML_NS) {
return node._qualifiedName === lowerQualifiedName;
}
return node._qualifiedName === qualifiedName;
}
})
});
}
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => domSymbolTree.treeToArray(root, {
filter(node) {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
return false;
}
return node._qualifiedName === qualifiedName;
}
})
});
};
exports.listOfElementsWithNamespaceAndLocalName = (namespace, localName, root) => {
// https://dom.spec.whatwg.org/#concept-getelementsbytagnamens
if (namespace === "") {
namespace = null;
}
if (namespace === "*" && localName === "*") {
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => domSymbolTree.treeToArray(root, {
filter: node => node.nodeType === NODE_TYPE.ELEMENT_NODE && node !== root
})
});
}
if (namespace === "*") {
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => domSymbolTree.treeToArray(root, {
filter(node) {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
return false;
}
return node._localName === localName;
}
})
});
}
if (localName === "*") {
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => domSymbolTree.treeToArray(root, {
filter(node) {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
return false;
}
return node._namespaceURI === namespace;
}
})
});
}
return HTMLCollection.createImpl(root._globalObject, [], {
element: root,
query: () => domSymbolTree.treeToArray(root, {
filter(node) {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE || node === root) {
return false;
}
return node._localName === localName && node._namespaceURI === namespace;
}
})
});
};
// https://dom.spec.whatwg.org/#converting-nodes-into-a-node
// create a fragment (or just return a node for one item)
exports.convertNodesIntoNode = (document, nodes) => {
if (nodes.length === 1) { // note: I'd prefer to check instanceof Node rather than string
return typeof nodes[0] === "string" ? document.createTextNode(nodes[0]) : nodes[0];
}
const fragment = document.createDocumentFragment();
for (let i = 0; i < nodes.length; i++) {
fragment._append(typeof nodes[i] === "string" ? document.createTextNode(nodes[i]) : nodes[i]);
}
return fragment;
};
// https://dom.spec.whatwg.org/#locate-a-namespace-prefix
exports.locateNamespacePrefix = (element, namespace) => {
if (element._namespaceURI === namespace && element._prefix !== null) {
return element._prefix;
}
for (const attribute of element._attributeList) {
if (attribute._namespacePrefix === "xmlns" && attribute._value === namespace) {
return attribute._localName;
}
}
if (element.parentElement !== null) {
return exports.locateNamespacePrefix(element.parentElement, namespace);
}
return null;
};
// https://dom.spec.whatwg.org/#locate-a-namespace
exports.locateNamespace = (node, prefix) => {
switch (node.nodeType) {
case NODE_TYPE.ELEMENT_NODE: {
if (node._namespaceURI !== null && node._prefix === prefix) {
return node._namespaceURI;
}
if (prefix === null) {
for (const attribute of node._attributeList) {
if (attribute._namespace === XMLNS_NS &&
attribute._namespacePrefix === null &&
attribute._localName === "xmlns") {
return attribute._value !== "" ? attribute._value : null;
}
}
} else {
for (const attribute of node._attributeList) {
if (attribute._namespace === XMLNS_NS &&
attribute._namespacePrefix === "xmlns" &&
attribute._localName === prefix) {
return attribute._value !== "" ? attribute._value : null;
}
}
}
if (node.parentElement === null) {
return null;
}
return exports.locateNamespace(node.parentElement, prefix);
}
case NODE_TYPE.DOCUMENT_NODE: {
if (node.documentElement === null) {
return null;
}
return exports.locateNamespace(node.documentElement, prefix);
}
case NODE_TYPE.DOCUMENT_TYPE_NODE:
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
return null;
}
case NODE_TYPE.ATTRIBUTE_NODE: {
if (node._element === null) {
return null;
}
return exports.locateNamespace(node._element, prefix);
}
default: {
if (node.parentElement === null) {
return null;
}
return exports.locateNamespace(node.parentElement, prefix);
}
}
};