"use strict"; const EventTargetImpl = require("../events/EventTarget-impl").implementation; const { domSymbolTree } = require("../helpers/internal-constants"); const { simultaneousIterators } = require("../../utils"); const DOMException = require("domexception"); const NODE_TYPE = require("../node-type"); const NODE_DOCUMENT_POSITION = require("../node-document-position"); const NodeList = require("../generated/NodeList"); const { documentBaseURLSerialized } = require("../helpers/document-base-url"); const { clone, locateNamespacePrefix, locateNamespace } = require("../node"); const attributes = require("../attributes"); function isObsoleteNodeType(node) { return node.nodeType === NODE_TYPE.ENTITY_NODE || node.nodeType === NODE_TYPE.ENTITY_REFERENCE_NODE || node.nodeType === NODE_TYPE.NOTATION_NODE || // node.nodeType === NODE_TYPE.ATTRIBUTE_NODE || // this is missing how do we handle? node.nodeType === NODE_TYPE.CDATA_SECTION_NODE; } function nodeEquals(a, b) { if (a.nodeType !== b.nodeType) { return false; } switch (a.nodeType) { case NODE_TYPE.DOCUMENT_TYPE_NODE: if (a.name !== b.name || a.publicId !== b.publicId || a.systemId !== b.systemId) { return false; } break; case NODE_TYPE.ELEMENT_NODE: if (a._namespaceURI !== b._namespaceURI || a._prefix !== b._prefix || a._localName !== b._localName || a._attributes.length !== b._attributes.length) { return false; } break; case NODE_TYPE.PROCESSING_INSTRUCTION_NODE: if (a._target !== b._target || a._data !== b._data) { return false; } break; case NODE_TYPE.TEXT_NODE: case NODE_TYPE.COMMENT_NODE: if (a._data !== b._data) { return false; } break; } if (a.nodeType === NODE_TYPE.ELEMENT_NODE && !attributes.attributeListsEqual(a, b)) { return false; } for (const nodes of simultaneousIterators(domSymbolTree.childrenIterator(a), domSymbolTree.childrenIterator(b))) { if (!nodes[0] || !nodes[1]) { // mismatch in the amount of childNodes return false; } if (!nodeEquals(nodes[0], nodes[1])) { return false; } } return true; } class NodeImpl extends EventTargetImpl { constructor(args, privateData) { super(); domSymbolTree.initialize(this); this._ownerDocument = privateData.ownerDocument; this._childNodesList = null; this._childrenList = null; this._version = 0; this._memoizedQueries = {}; } get parentNode() { return domSymbolTree.parent(this); } getRootNode() { // ignore option for composed, because of no Shadow DOM support let root; for (const ancestor of domSymbolTree.ancestorsIterator(this)) { root = ancestor; } return root; } get nodeName() { switch (this.nodeType) { case NODE_TYPE.ELEMENT_NODE: return this.tagName; case NODE_TYPE.TEXT_NODE: return "#text"; case NODE_TYPE.CDATA_SECTION_NODE: return "#cdata-section"; case NODE_TYPE.PROCESSING_INSTRUCTION_NODE: return this.target; case NODE_TYPE.COMMENT_NODE: return "#comment"; case NODE_TYPE.DOCUMENT_NODE: return "#document"; case NODE_TYPE.DOCUMENT_TYPE_NODE: return this.name; case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: return "#document-fragment"; } // should never happen return null; } get firstChild() { return domSymbolTree.firstChild(this); } get isConnected() { for (const ancestor of domSymbolTree.ancestorsIterator(this)) { if (ancestor.nodeType === NODE_TYPE.DOCUMENT_NODE) { return true; } } return false; } get ownerDocument() { return this.nodeType === NODE_TYPE.DOCUMENT_NODE ? null : this._ownerDocument; } get lastChild() { return domSymbolTree.lastChild(this); } get childNodes() { if (!this._childNodesList) { this._childNodesList = NodeList.createImpl([], { element: this, query: () => domSymbolTree.childrenToArray(this) }); } else { this._childNodesList._update(); } return this._childNodesList; } get nextSibling() { return domSymbolTree.nextSibling(this); } get previousSibling() { return domSymbolTree.previousSibling(this); } insertBefore(newChildImpl, refChildImpl) { // DocumentType must be implicitly adopted if (newChildImpl.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) { newChildImpl._ownerDocument = this._ownerDocument; } if (newChildImpl.nodeType && newChildImpl.nodeType === NODE_TYPE.ATTRIBUTE_NODE) { throw new DOMException("The operation would yield an incorrect node tree.", "HierarchyRequestError"); } if (this._ownerDocument !== newChildImpl._ownerDocument) { // adopt the node when it's not in this document this._ownerDocument.adoptNode(newChildImpl); } else { // search for parents matching the newChild for (const ancestor of domSymbolTree.ancestorsIterator(this)) { if (ancestor === newChildImpl) { throw new DOMException("The operation would yield an incorrect node tree.", "HierarchyRequestError"); } } } // fragments are merged into the element (except parser-created fragments in <template>) if (newChildImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) { let grandChildImpl; while ((grandChildImpl = domSymbolTree.firstChild(newChildImpl))) { newChildImpl.removeChild(grandChildImpl); this.insertBefore(grandChildImpl, refChildImpl); } } else if (newChildImpl === refChildImpl) { return newChildImpl; } else { const oldParentImpl = domSymbolTree.parent(newChildImpl); // if the newChild is already in the tree elsewhere, remove it first if (oldParentImpl) { oldParentImpl.removeChild(newChildImpl); } if (refChildImpl === null) { domSymbolTree.appendChild(this, newChildImpl); } else { if (domSymbolTree.parent(refChildImpl) !== this) { throw new DOMException("The object can not be found here.", "NotFoundError"); } domSymbolTree.insertBefore(refChildImpl, newChildImpl); } this._modified(); if (newChildImpl.nodeType === NODE_TYPE.TEXT_NODE) { this._childTextContentChangeSteps(); } if (this._attached && newChildImpl._attach) { newChildImpl._attach(); } this._descendantAdded(this, newChildImpl); } return newChildImpl; } // raises(DOMException); _modified() { this._version++; for (const ancestor of domSymbolTree.ancestorsIterator(this)) { ancestor._version++; } if (this._childrenList) { this._childrenList._update(); } if (this._childNodesList) { this._childNodesList._update(); } this._clearMemoizedQueries(); } _childTextContentChangeSteps() { // Default: do nothing } _clearMemoizedQueries() { this._memoizedQueries = {}; const myParent = domSymbolTree.parent(this); if (myParent) { myParent._clearMemoizedQueries(); } } _descendantRemoved(parent, child) { const myParent = domSymbolTree.parent(this); if (myParent) { myParent._descendantRemoved(parent, child); } } _descendantAdded(parent, child) { const myParent = domSymbolTree.parent(this); if (myParent) { myParent._descendantAdded(parent, child); } } replaceChild(node, child) { this.insertBefore(node, child); return this.removeChild(child); } _attach() { this._attached = true; for (const child of domSymbolTree.childrenIterator(this)) { if (child._attach) { child._attach(); } } } _detach() { this._attached = false; if (this._ownerDocument && this._ownerDocument._lastFocusedElement === this) { this._ownerDocument._lastFocusedElement = null; } for (const child of domSymbolTree.childrenIterator(this)) { if (child._detach) { child._detach(); } } } removeChild(/* Node */ oldChildImpl) { if (!oldChildImpl || domSymbolTree.parent(oldChildImpl) !== this) { throw new DOMException("The object can not be found here.", "NotFoundError"); } if (this._ownerDocument) { this._ownerDocument._runPreRemovingSteps(oldChildImpl); } domSymbolTree.remove(oldChildImpl); this._modified(); oldChildImpl._detach(); this._descendantRemoved(this, oldChildImpl); if (oldChildImpl.nodeType === NODE_TYPE.TEXT_NODE) { this._childTextContentChangeSteps(); } return oldChildImpl; } // raises(DOMException); appendChild(newChild) { return this.insertBefore(newChild, null); } hasChildNodes() { return domSymbolTree.hasChildren(this); } normalize() { for (const child of domSymbolTree.childrenIterator(this)) { if (child.normalize) { child.normalize(); } // Normalize should only transform Text nodes, and nothing else. if (child.nodeType !== NODE_TYPE.TEXT_NODE) { continue; } if (child.nodeValue === "") { this.removeChild(child); continue; } const prevChild = domSymbolTree.previousSibling(child); if (prevChild && prevChild.nodeType === NODE_TYPE.TEXT_NODE) { // merge text nodes prevChild.appendData(child.nodeValue); this.removeChild(child); } } } get parentElement() { const parentNode = domSymbolTree.parent(this); return parentNode !== null && parentNode.nodeType === NODE_TYPE.ELEMENT_NODE ? parentNode : null; } get baseURI() { return documentBaseURLSerialized(this._ownerDocument); } compareDocumentPosition(otherImpl) { // Let reference be the context object. const reference = this; if (isObsoleteNodeType(reference) || isObsoleteNodeType(otherImpl)) { throw new Error("Obsolete node type"); } const result = domSymbolTree.compareTreePosition(reference, otherImpl); // “If other and reference are not in the same tree, return the result of adding DOCUMENT_POSITION_DISCONNECTED, // DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either DOCUMENT_POSITION_PRECEDING or // DOCUMENT_POSITION_FOLLOWING, with the constraint that this is to be consistent, together.” if (result === NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED) { // symbol-tree does not add these bits required by the spec: return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED | NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_FOLLOWING; } return result; } lookupPrefix(namespace) { if (namespace === null || namespace === "") { return null; } switch (this.nodeType) { case NODE_TYPE.ELEMENT_NODE: { return locateNamespacePrefix(this, namespace); } case NODE_TYPE.DOCUMENT_NODE: { return this.documentElement !== null ? locateNamespacePrefix(this.documentElement, namespace) : null; } case NODE_TYPE.DOCUMENT_TYPE_NODE: case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: { return null; } case NODE_TYPE.ATTRIBUTE_NODE: { return this._element !== null ? locateNamespacePrefix(this._element, namespace) : null; } default: { return this.parentElement !== null ? locateNamespacePrefix(this.parentElement, namespace) : null; } } } lookupNamespaceURI(prefix) { if (prefix === "") { prefix = null; } return locateNamespace(this, prefix); } isDefaultNamespace(namespace) { if (namespace === "") { namespace = null; } const defaultNamespace = locateNamespace(this, null); return defaultNamespace === namespace; } contains(other) { if (other === null) { return false; } else if (this === other) { return true; } return Boolean(this.compareDocumentPosition(other) & NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_CONTAINED_BY); } isEqualNode(node) { if (node === null) { return false; } // Fast-path, not in the spec if (this === node) { return true; } return nodeEquals(this, node); } isSameNode(node) { if (this === node) { return true; } return false; } cloneNode(deep) { deep = Boolean(deep); return clone(this, undefined, deep); } get nodeValue() { switch (this.nodeType) { case NODE_TYPE.ATTRIBUTE_NODE: { return this._value; } case NODE_TYPE.TEXT_NODE: case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text case NODE_TYPE.PROCESSING_INSTRUCTION_NODE: case NODE_TYPE.COMMENT_NODE: { return this._data; } default: { return null; } } } set nodeValue(value) { if (value === null) { value = ""; } switch (this.nodeType) { case NODE_TYPE.ATTRIBUTE_NODE: { attributes.setAnExistingAttributeValue(this, value); break; } case NODE_TYPE.TEXT_NODE: case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text case NODE_TYPE.PROCESSING_INSTRUCTION_NODE: case NODE_TYPE.COMMENT_NODE: { this.replaceData(0, this.length, value); break; } } } get textContent() { switch (this.nodeType) { case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: case NODE_TYPE.ELEMENT_NODE: { let text = ""; for (const child of domSymbolTree.treeIterator(this)) { if (child.nodeType === NODE_TYPE.TEXT_NODE || child.nodeType === NODE_TYPE.CDATA_SECTION_NODE) { text += child.nodeValue; } } return text; } case NODE_TYPE.ATTRIBUTE_NODE: { return this._value; } case NODE_TYPE.TEXT_NODE: case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text case NODE_TYPE.PROCESSING_INSTRUCTION_NODE: case NODE_TYPE.COMMENT_NODE: { return this._data; } default: { return null; } } } set textContent(value) { switch (this.nodeType) { case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: case NODE_TYPE.ELEMENT_NODE: { let child = domSymbolTree.firstChild(this); while (child) { this.removeChild(child); child = domSymbolTree.firstChild(this); } if (value !== null && value !== "") { this.appendChild(this._ownerDocument.createTextNode(value)); } break; } case NODE_TYPE.ATTRIBUTE_NODE: { attributes.setAnExistingAttributeValue(this, value); break; } case NODE_TYPE.TEXT_NODE: case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text case NODE_TYPE.PROCESSING_INSTRUCTION_NODE: case NODE_TYPE.COMMENT_NODE: { this.replaceData(0, this.length, value); break; } } } } module.exports = { implementation: NodeImpl };