All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.htmlunit.javascript.host.dom.Node Maven / Gradle / Ivy

Go to download

XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.

There is a newer version: 8.4.1
Show newest version
/*
 * Copyright (c) 2002-2024 Gargoyle Software Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.htmlunit.javascript.host.dom;

import static org.htmlunit.BrowserVersionFeatures.JS_NODE_CONTAINS_RETURNS_FALSE_FOR_INVALID_ARG;
import static org.htmlunit.BrowserVersionFeatures.JS_NODE_INSERT_BEFORE_REF_OPTIONAL;
import static org.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
import static org.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
import static org.htmlunit.javascript.configuration.SupportedBrowser.FF;
import static org.htmlunit.javascript.configuration.SupportedBrowser.FF_ESR;
import static org.htmlunit.javascript.configuration.SupportedBrowser.IE;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;

import org.htmlunit.SgmlPage;
import org.htmlunit.corejs.javascript.Context;
import org.htmlunit.corejs.javascript.Function;
import org.htmlunit.corejs.javascript.Interpreter;
import org.htmlunit.corejs.javascript.JavaScriptException;
import org.htmlunit.corejs.javascript.RhinoException;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.corejs.javascript.Undefined;
import org.htmlunit.html.DomDocumentFragment;
import org.htmlunit.html.DomElement;
import org.htmlunit.html.DomNode;
import org.htmlunit.html.HtmlElement;
import org.htmlunit.html.HtmlInlineFrame;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.JavaScriptEngine;
import org.htmlunit.javascript.configuration.JsxClass;
import org.htmlunit.javascript.configuration.JsxConstant;
import org.htmlunit.javascript.configuration.JsxConstructor;
import org.htmlunit.javascript.configuration.JsxFunction;
import org.htmlunit.javascript.configuration.JsxGetter;
import org.htmlunit.javascript.configuration.JsxSetter;
import org.htmlunit.javascript.host.Element;
import org.htmlunit.javascript.host.NamedNodeMap;
import org.htmlunit.javascript.host.Window;
import org.htmlunit.javascript.host.event.EventTarget;
import org.htmlunit.javascript.host.html.HTMLCollection;
import org.htmlunit.javascript.host.html.HTMLDocument;
import org.htmlunit.javascript.host.html.HTMLHtmlElement;

/**
 * The JavaScript object {@code Node} which is the base class for all DOM
 * objects. This will typically wrap an instance of {@link DomNode}.
 *
 * @author Mike Bowler
 * @author David K. Taylor
 * @author Barnaby Court
 * @author Christian Sell
 * @author George Murnock
 * @author Chris Erskine
 * @author Bruce Faulkner
 * @author Ahmed Ashour
 * @author Ronald Brill
 * @author Frank Danek
 */
@JsxClass
public class Node extends EventTarget {

    /** @see org.w3c.dom.Node#ELEMENT_NODE */
    @JsxConstant
    public static final int ELEMENT_NODE = org.w3c.dom.Node.ELEMENT_NODE;

    /** @see org.w3c.dom.Node#ATTRIBUTE_NODE */
    @JsxConstant
    public static final int ATTRIBUTE_NODE = org.w3c.dom.Node.ATTRIBUTE_NODE;

    /** @see org.w3c.dom.Node#TEXT_NODE */
    @JsxConstant
    public static final int TEXT_NODE = org.w3c.dom.Node.TEXT_NODE;

    /** @see org.w3c.dom.Node#CDATA_SECTION_NODE */
    @JsxConstant
    public static final int CDATA_SECTION_NODE = org.w3c.dom.Node.CDATA_SECTION_NODE;

    /** @see org.w3c.dom.Node#ENTITY_REFERENCE_NODE */
    @JsxConstant
    public static final int ENTITY_REFERENCE_NODE = org.w3c.dom.Node.ENTITY_REFERENCE_NODE;

    /** @see org.w3c.dom.Node#ENTITY_NODE */
    @JsxConstant
    public static final int ENTITY_NODE = org.w3c.dom.Node.ENTITY_NODE;

    /** @see org.w3c.dom.Node#PROCESSING_INSTRUCTION_NODE */
    @JsxConstant
    public static final int PROCESSING_INSTRUCTION_NODE = org.w3c.dom.Node.PROCESSING_INSTRUCTION_NODE;

    /** @see org.w3c.dom.Node#COMMENT_NODE */
    @JsxConstant
    public static final int COMMENT_NODE = org.w3c.dom.Node.COMMENT_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_NODE */
    @JsxConstant
    public static final int DOCUMENT_NODE = org.w3c.dom.Node.DOCUMENT_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_TYPE_NODE */
    @JsxConstant
    public static final int DOCUMENT_TYPE_NODE = org.w3c.dom.Node.DOCUMENT_TYPE_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_FRAGMENT_NODE */
    @JsxConstant
    public static final int DOCUMENT_FRAGMENT_NODE = org.w3c.dom.Node.DOCUMENT_FRAGMENT_NODE;

    /** @see org.w3c.dom.Node#NOTATION_NODE */
    @JsxConstant
    public static final int NOTATION_NODE = org.w3c.dom.Node.NOTATION_NODE;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_DISCONNECTED */
    @JsxConstant
    public static final int DOCUMENT_POSITION_DISCONNECTED = org.w3c.dom.Node.DOCUMENT_POSITION_DISCONNECTED;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_PRECEDING */
    @JsxConstant
    public static final int DOCUMENT_POSITION_PRECEDING = org.w3c.dom.Node.DOCUMENT_POSITION_PRECEDING;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_FOLLOWING */
    @JsxConstant
    public static final int DOCUMENT_POSITION_FOLLOWING = org.w3c.dom.Node.DOCUMENT_POSITION_FOLLOWING;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_CONTAINS */
    @JsxConstant
    public static final int DOCUMENT_POSITION_CONTAINS = org.w3c.dom.Node.DOCUMENT_POSITION_CONTAINS;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_CONTAINED_BY */
    @JsxConstant
    public static final int DOCUMENT_POSITION_CONTAINED_BY = org.w3c.dom.Node.DOCUMENT_POSITION_CONTAINED_BY;

    /** @see org.w3c.dom.Node#DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC */
    @JsxConstant
    public static final int DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
        = org.w3c.dom.Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;

    /** "Live" child nodes collection; has to be a member to have equality (==) working. */
    private NodeList childNodes_;

    /**
     * Creates an instance.
     */
    public Node() {
        // Empty.
    }

    /**
     * JavaScript constructor.
     */
    @Override
    @JsxConstructor({CHROME, EDGE, FF, FF_ESR})
    public void jsConstructor() {
        super.jsConstructor();
    }

    /**
     * Gets the JavaScript property {@code nodeType} for the current node.
     * @return the node type
     */
    @JsxGetter
    public int getNodeType() {
        return getDomNodeOrDie().getNodeType();
    }

    /**
     * Gets the JavaScript property {@code nodeName} for the current node.
     * @return the node name
     */
    @JsxGetter
    public String getNodeName() {
        return getDomNodeOrDie().getNodeName();
    }

    /**
     * Gets the JavaScript property {@code nodeValue} for the current node.
     * @return the node value
     */
    @JsxGetter
    public String getNodeValue() {
        return getDomNodeOrDie().getNodeValue();
    }

    /**
     * Sets the JavaScript property {@code nodeValue} for the current node.
     * @param newValue the new node value
     */
    @JsxSetter
    public void setNodeValue(final String newValue) {
        getDomNodeOrDie().setNodeValue(newValue);
    }

    /**
     * Adds a DOM node to the node.
     * @param childObject the node to add to this node
     * @return the newly added child node
     */
    @JsxFunction
    public Object appendChild(final Object childObject) {
        Object appendedChild = null;
        if (childObject instanceof Node) {
            final Node childNode = (Node) childObject;

            // is the node allowed here?
            if (!isNodeInsertable(childNode)) {
                throw asJavaScriptException(
                    new DOMException("Node cannot be inserted at the specified point in the hierarchy",
                        DOMException.HIERARCHY_REQUEST_ERR));
            }

            // Get XML node for the DOM node passed in
            final DomNode childDomNode = childNode.getDomNodeOrDie();

            // Get the parent XML node that the child should be added to.
            final DomNode parentNode = getDomNodeOrDie();

            // Append the child to the parent node
            parentNode.appendChild(childDomNode);
            appendedChild = childObject;

            initInlineFrameIfNeeded(childDomNode);
            for (final HtmlElement htmlElement : childDomNode.getHtmlElementDescendants()) {
                initInlineFrameIfNeeded(htmlElement);
            }
        }
        return appendedChild;
    }

    /**
     * If we have added a new iframe that
     * had no source attribute, we have to take care the
     * 'onload' handler is triggered.
     */
    private static void initInlineFrameIfNeeded(final DomNode childDomNode) {
        if (childDomNode instanceof HtmlInlineFrame) {
            final HtmlInlineFrame frame = (HtmlInlineFrame) childDomNode;
            if (DomElement.ATTRIBUTE_NOT_DEFINED == frame.getSrcAttribute()) {
                frame.loadInnerPage();
            }
        }
    }

    /**
     * Encapsulates the given {@link DOMException} into a Rhino-compatible exception.
     *
     * @param exception the exception to encapsulate
     * @return the created exception
     */
    protected RhinoException asJavaScriptException(final DOMException exception) {
        final Window w = getWindow();
        exception.setPrototype(w.getPrototype(exception.getClass()));
        exception.setParentScope(w);

        // get current line and file name
        // this method can only be used in interpreted mode. If one day we choose to use compiled mode,
        // then we'll have to find an other way here.
        final String fileName;
        final int lineNumber;
        if (Context.getCurrentContext().getOptimizationLevel() == -1) {
            final int[] linep = new int[1];
            final String sourceName = new Interpreter().getSourcePositionFromStack(Context.getCurrentContext(), linep);
            fileName = sourceName.replaceFirst("script in (.*) from .*", "$1");
            lineNumber = linep[0];
        }
        else {
            throw new Error("HtmlUnit not ready to run in compiled mode");
        }

        exception.setLocation(fileName, lineNumber);

        return new JavaScriptException(exception, fileName, lineNumber);
    }

    /**
     * Add a DOM node as a child to this node before the referenced node.
     * If the referenced node is null, append to the end.
     * @param context the JavaScript context
     * @param scope the scope
     * @param thisObj the scriptable
     * @param args the arguments passed into the method
     * @param function the function
     * @return the newly added child node
     */
    @JsxFunction
    public static Object insertBefore(final Context context, final Scriptable scope,
            final Scriptable thisObj, final Object[] args, final Function function) {
        return ((Node) thisObj).insertBeforeImpl(args);
    }

    /**
     * Add a DOM node as a child to this node before the referenced node.
     * If the referenced node is null, append to the end.
     * @param args the arguments
     * @return the newly added child node
     */
    protected Object insertBeforeImpl(final Object[] args) {
        if (args.length < 1) {
            throw JavaScriptEngine.typeError(
                    "Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only 0 present.");
        }

        final Object newChildObject = args[0];
        final Object refChildObject;
        if (args.length > 1) {
            refChildObject = args[1];
        }
        else {
            refChildObject = Undefined.instance;
        }
        Object insertedChild = null;

        if (newChildObject instanceof Node) {
            final Node newChild = (Node) newChildObject;

            // is the node allowed here?
            if (!isNodeInsertable(newChild)) {
                throw JavaScriptEngine.constructError("ReferenceError",
                        "Node cannot be inserted at the specified point in the hierarchy");
            }

            final DomNode newChildNode = newChild.getDomNodeOrDie();
            if (newChildNode instanceof DomDocumentFragment) {
                final DomDocumentFragment fragment = (DomDocumentFragment) newChildNode;
                for (final DomNode child : fragment.getChildren()) {
                    if (!isNodeInsertable(child.getScriptableObject())) {
                        throw JavaScriptEngine.constructError("ReferenceError",
                                "Node cannot be inserted at the specified point in the hierarchy");
                    }
                }
            }

            // extract refChild
            final DomNode refChildNode;
            if (JavaScriptEngine.isUndefined(refChildObject)) {
                if (args.length == 2 || getBrowserVersion().hasFeature(JS_NODE_INSERT_BEFORE_REF_OPTIONAL)) {
                    refChildNode = null;
                }
                else {
                    throw JavaScriptEngine.typeError(
                            "Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only 1 present.");
                }
            }
            else if (refChildObject == null) {
                refChildNode = null;
            }
            else {
                refChildNode = ((Node) refChildObject).getDomNodeOrDie();
            }

            final DomNode domNode = getDomNodeOrDie();

            try {
                domNode.insertBefore(newChildNode, refChildNode);
            }
            catch (final org.w3c.dom.DOMException e) {
                throw JavaScriptEngine.constructError("ReferenceError", e.getMessage());
            }
            insertedChild = newChild;
        }
        return insertedChild;
    }

    /**
     * Indicates if the node can be inserted.
     * @param childObject the node
     * @return {@code false} if it is not allowed here
     */
    private static boolean isNodeInsertable(final Node childObject) {
        if (childObject instanceof HTMLHtmlElement) {
            final DomNode domNode = childObject.getDomNodeOrDie();
            return domNode.getPage().getDocumentElement() != domNode;
        }
        return true;
    }

    /**
     * Removes the DOM node from its parent.
     * @see MDN documentation
     */
    protected void remove() {
        getDomNodeOrDie().remove();
    }

    /**
     * Removes a DOM node from this node.
     * @param childObject the node to remove from this node
     * @return the removed child node
     */
    @JsxFunction
    public Object removeChild(final Object childObject) {
        if (!(childObject instanceof Node)) {
            return null;
        }

        // Get XML node for the DOM node passed in
        final DomNode childNode = ((Node) childObject).getDomNodeOrDie();

        if (!getDomNodeOrDie().isAncestorOf(childNode)) {
            throw JavaScriptEngine.throwAsScriptRuntimeEx(
                    new Exception("NotFoundError: Failed to execute 'removeChild' on '"
                        + this + "': The node to be removed is not a child of this node."));
        }
        // Remove the child from the parent node
        childNode.remove();
        return childObject;
    }

    /**
     * Replaces a child DOM node with another DOM node.
     * @param newChildObject the node to add as a child of this node
     * @param oldChildObject the node to remove as a child of this node
     * @return the removed child node
     */
    @JsxFunction
    public Object replaceChild(final Object newChildObject, final Object oldChildObject) {
        Object removedChild = null;

        if (newChildObject instanceof DocumentFragment) {
            final DocumentFragment fragment = (DocumentFragment) newChildObject;
            Node firstNode = null;
            final Node refChildObject = ((Node) oldChildObject).getNextSibling();
            for (final DomNode node : fragment.getDomNodeOrDie().getChildren()) {
                if (firstNode == null) {
                    replaceChild(node.getScriptableObject(), oldChildObject);
                    firstNode = node.getScriptableObject();
                }
                else {
                    insertBeforeImpl(new Object[] {node.getScriptableObject(), refChildObject});
                }
            }
            if (firstNode == null) {
                removeChild(oldChildObject);
            }
            removedChild = oldChildObject;
        }
        else if (newChildObject instanceof Node && oldChildObject instanceof Node) {
            final Node newChild = (Node) newChildObject;

            // is the node allowed here?
            if (!isNodeInsertable(newChild)) {
                throw JavaScriptEngine.reportRuntimeError(
                        "Node cannot be inserted at the specified point in the hierarchy");
            }

            // Get XML nodes for the DOM nodes passed in
            final DomNode newChildNode = newChild.getDomNodeOrDie();
            final DomNode oldChildNode = ((Node) oldChildObject).getDomNodeOrDie();

            // Replace the old child with the new child.
            oldChildNode.replace(newChildNode);
            removedChild = oldChildObject;
        }

        return removedChild;
    }

    /**
     * Clones this node.
     * @param deep if {@code true}, recursively clones all descendants
     * @return the newly cloned node
     */
    @JsxFunction
    public Object cloneNode(final boolean deep) {
        final DomNode domNode = getDomNodeOrDie();
        final DomNode clonedNode = domNode.cloneNode(deep);

        return getJavaScriptNode(clonedNode);
    }

    /**
     * Check if 2 nodes are equals.
     * For detail specifications
     * @see concept-node-equals
     * @param other the node to compare with
     * @return true or false
     */
    @JsxFunction
    public boolean isEqualNode(final Node other) {
        if (isSameNode(other)) {
            return true;
        }

        if (!getClassName().equals(other.getClassName())) {
            return false;
        }

        if (this instanceof DocumentType) {
            final DocumentType docType = (DocumentType) this;
            final DocumentType otherDocType = (DocumentType) other;
            if (!Objects.equals(docType.getName(), otherDocType.getName())
                    || !Objects.equals(docType.getPublicId(), otherDocType.getPublicId())
                    || !Objects.equals(docType.getSystemId(), otherDocType.getSystemId())) {
                return false;
            }

        }
        else if (this instanceof Element) {
            final Element element = (Element) this;
            final Element otherElement = (Element) other;
            if (!Objects.equals(element.getNodeName(), otherElement.getNodeName())
                    || !Objects.equals(element.getPrefix(), otherElement.getPrefix())
                    || !Objects.equals(element.getLocalName(), otherElement.getLocalName())) {
                return false;
            }

            final NamedNodeMap attributesMap = element.getAttributes();
            final NamedNodeMap otherAttributesMap = otherElement.getAttributes();
            if (attributesMap != null || otherAttributesMap != null) {
                if (attributesMap == null || otherAttributesMap == null) {
                    return false;
                }

                final int length = attributesMap.getLength();
                if (length != otherAttributesMap.getLength()) {
                    return false;
                }

                final Map name2Attributes = new HashMap<>();
                for (int i = 0; i < length; i++) {
                    final Attr attribute = (Attr) attributesMap.item(i);
                    name2Attributes.put(attribute.getName(), attribute);
                }

                for (int i = 0; i < length; i++) {
                    final Attr otherAttribute = (Attr) otherAttributesMap.item(i);
                    final Attr attribute = name2Attributes.get(otherAttribute.getName());
                    if (attribute == null) {
                        return false;
                    }
                    if (!attribute.isEqualNode(otherAttribute)) {
                        return false;
                    }
                }
            }

        }
        else if (this instanceof Attr) {
            final Attr attr = (Attr) this;
            final Attr otherAttr = (Attr) other;
            if (!Objects.equals(attr.getName(), otherAttr.getName())
                    || !Objects.equals(attr.getLocalName(), otherAttr.getLocalName())
                    || !Objects.equals(attr.getValue(), otherAttr.getValue())) {
                return false;
            }

        }
        else if (this instanceof ProcessingInstruction) {
            final ProcessingInstruction instruction = (ProcessingInstruction) this;
            final ProcessingInstruction otherInstruction = (ProcessingInstruction) other;
            if (!Objects.equals(instruction.getTarget(), otherInstruction.getTarget())
                    || !Objects.equals(instruction.getData(), otherInstruction.getData())) {
                return false;
            }

        }
        else if (this instanceof Text || this instanceof Comment) {
            final CharacterData data = (CharacterData) this;
            final CharacterData otherData = (CharacterData) other;
            if (!Objects.equals(data.getData(), otherData.getData())) {
                return false;
            }
        }

        final NodeList childNodes = getChildNodes();
        final NodeList otherChildNodes = other.getChildNodes();
        if (childNodes != null || otherChildNodes != null) {
            if (childNodes == null || otherChildNodes == null) {
                return false;
            }

            final int length = childNodes.getLength();
            final int otherLength = childNodes.getLength();
            if (length != otherLength) {
                return false;
            }

            for (int i = 0; i < length; i++) {
                final Node childNode = (Node) childNodes.item(i);
                final Node otherChildNode = (Node) otherChildNodes.item(i);
                if (!childNode.isEqualNode(otherChildNode)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * This method provides a way to determine whether two Node references returned by
     * the implementation reference the same object.
     * When two Node references are references to the same object, even if through a proxy,
     * the references may be used completely interchangeably, such that all attributes
     * have the same values and calling the same DOM method on either reference always has exactly the same effect.
     *
     * @param other the node to test against
     *
     * @return whether this node is the same node as the given one
     */
    @JsxFunction
    public boolean isSameNode(final Object other) {
        return other == this;
    }

    /**
     * Returns whether this node has any children.
     * @return boolean true if this node has any children, false otherwise
     */
    @JsxFunction
    public boolean hasChildNodes() {
        return getDomNodeOrDie().getChildren().iterator().hasNext();
    }

    /**
     * Returns the child nodes of the current element.
     * @return the child nodes of the current element
     */
    @JsxGetter
    public NodeList getChildNodes() {
        if (childNodes_ == null) {
            final DomNode node = getDomNodeOrDie();
            childNodes_ = new NodeList(node, false);
            childNodes_.setElementsSupplier(
                    (Supplier> & Serializable)
                    () -> {
                        final List response = new ArrayList<>();
                        for (final DomNode child : node.getChildren()) {
                            response.add(child);
                        }

                        return response;
                    });
        }
        return childNodes_;
    }

    /**
     * Returns this node's parent node.
     * @return this node's parent node
     */
    public final Node getParent() {
        return getJavaScriptNode(getDomNodeOrDie().getParentNode());
    }

    /**
     * Gets the JavaScript property {@code parentNode} for the node that
     * contains the current node.
     * @return the parent node
     */
    @JsxGetter
    public Object getParentNode() {
        return getJavaScriptNode(getDomNodeOrDie().getParentNode());
    }

    /**
     * Gets the JavaScript property {@code nextSibling} for the node that
     * contains the current node.
     * @return the next sibling node or null if the current node has
     * no next sibling.
     */
    @JsxGetter
    public Node getNextSibling() {
        return getJavaScriptNode(getDomNodeOrDie().getNextSibling());
    }

    /**
     * Gets the JavaScript property {@code previousSibling} for the node that
     * contains the current node.
     * @return the previous sibling node or null if the current node has
     * no previous sibling.
     */
    @JsxGetter
    public Node getPreviousSibling() {
        return getJavaScriptNode(getDomNodeOrDie().getPreviousSibling());
    }

    /**
     * Gets the JavaScript property {@code firstChild} for the node that
     * contains the current node.
     * @return the first child node or null if the current node has
     * no children.
     */
    @JsxGetter
    public Node getFirstChild() {
        return getJavaScriptNode(getDomNodeOrDie().getFirstChild());
    }

    /**
     * Gets the JavaScript property {@code lastChild} for the node that
     * contains the current node.
     * @return the last child node or null if the current node has
     * no children.
     */
    @JsxGetter
    public Node getLastChild() {
        return getJavaScriptNode(getDomNodeOrDie().getLastChild());
    }

    /**
     * Gets the JavaScript node for a given DomNode.
     * @param domNode the DomNode
     * @return the JavaScript node or null if the DomNode was null
     */
    protected Node getJavaScriptNode(final DomNode domNode) {
        if (domNode == null) {
            return null;
        }
        return (Node) getScriptableFor(domNode);
    }

    /**
     * Returns the owner document.
     * @return the document
     */
    @JsxGetter
    public HtmlUnitScriptable getOwnerDocument() {
        final Object document = getDomNodeOrDie().getOwnerDocument();
        if (document != null) {
            return ((SgmlPage) document).getScriptableObject();
        }
        return null;
    }

    /**
     * Returns the owner document.
     * @return the document
     */
    @JsxFunction({CHROME, EDGE, FF, FF_ESR})
    public Object getRootNode() {
        Node parent = this;
        while (parent != null) {
            if (parent instanceof Document || parent instanceof DocumentFragment) {
                return parent;
            }
            parent = parent.getParent();
        }
        return this;
    }

    /**
     * Compares the positions of this node and the provided node within the document.
     * @param node node object that specifies the node to check
     * @return how the node is positioned relatively to the reference node.
     * @see DOM level 3
     * @see org.w3c.dom.Node#compareDocumentPosition(org.w3c.dom.Node)
     */
    @JsxFunction
    public int compareDocumentPosition(final Object node) {
        if (!(node instanceof Node)) {
            throw JavaScriptEngine.reportRuntimeError("Could not convert JavaScript argument arg 0");
        }
        return getDomNodeOrDie().compareDocumentPosition(((Node) node).getDomNodeOrDie());
    }

    /**
     * Merges adjacent TextNode objects to produce a normalized document object model.
     */
    @JsxFunction
    public void normalize() {
        getDomNodeOrDie().normalize();
    }

    /**
     * Gets the textContent attribute.
     * @return the contents of this node as text
     */
    @JsxGetter
    public String getTextContent() {
        return getDomNodeOrDie().getTextContent();
    }

    /**
     * Replace all children elements of this element with the supplied value.
     * @param value - the new value for the contents of this node
     */
    @JsxSetter
    public void setTextContent(final Object value) {
        getDomNodeOrDie().setTextContent(value == null ? null : JavaScriptEngine.toString(value));
    }

    /**
     * Gets the JavaScript property {@code parentElement}.
     * @return the parent element
     * @see #getParentNode()
     */
    @JsxGetter({CHROME, EDGE, FF, FF_ESR})
    public Element getParentElement() {
        final Node parent = getParent();
        if (!(parent instanceof Element)) {
            return null;
        }
        return (Element) parent;
    }

    /**
     * Returns the attributes of this XML element.
     * @see Gecko DOM Reference
     * @return the attributes of this XML element
     */
    @JsxGetter(IE)
    public Object getAttributes() {
        return null;
    }

    /**
     * Checks whether the given element is contained within this object.
     * @param element element object that specifies the element to check
     * @return true if the element is contained within this object
     */
    @JsxFunction({CHROME, EDGE, FF, FF_ESR})
    public boolean contains(final Object element) {
        if (element == null || JavaScriptEngine.isUndefined(element)) {
            return false;
        }

        if (!(element instanceof Node)) {
            if (getBrowserVersion().hasFeature(JS_NODE_CONTAINS_RETURNS_FALSE_FOR_INVALID_ARG)) {
                return false;
            }
            throw JavaScriptEngine.reportRuntimeError("Could not convert JavaScript argument arg 0");
        }

        if (getBrowserVersion().hasFeature(JS_NODE_CONTAINS_RETURNS_FALSE_FOR_INVALID_ARG)) {
            if (element instanceof CharacterData) {
                return false;
            }
            if (this instanceof CharacterData) {
                throw JavaScriptEngine.reportRuntimeError("Function 'contains' not available for text nodes.");
            }
        }

        for (Node parent = (Node) element; parent != null; parent = parent.getParentElement()) {
            if (this == parent) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the Base URI as a string.
     * @return the Base URI as a string
     */
    @JsxGetter({CHROME, EDGE, FF, FF_ESR})
    public String getBaseURI() {
        return getDomNodeOrDie().getBaseURI();
    }

    /**
     * Returns true when the current element has any attributes or not.
     * @return true if an attribute is specified on this element
     */
    @JsxFunction(IE)
    public boolean hasAttributes() {
        return getDomNodeOrDie().hasAttributes();
    }

    /**
     * Returns the namespace prefix.
     * @return the namespace prefix
     */
    @JsxGetter(IE)
    public Object getPrefix() {
        return getDomNodeOrDie().getPrefix();
    }

    /**
     * Returns the local name of this attribute.
     * @return the local name of this attribute
     */
    @JsxGetter(IE)
    public Object getLocalName() {
        return getDomNodeOrDie().getLocalName();
    }

    /**
     * Returns the URI that identifies an XML namespace.
     * @return the URI that identifies an XML namespace
     */
    @JsxGetter(IE)
    public Object getNamespaceURI() {
        return getDomNodeOrDie().getNamespaceURI();
    }

    /**
     * Returns the current number of child elements.
     * @return the child element count
     */
    protected int getChildElementCount() {
        final DomNode domNode = getDomNodeOrDie();
        if (domNode instanceof DomElement) {
            return ((DomElement) domNode).getChildElementCount();
        }

        int counter = 0;
        for (final DomNode child : getDomNodeOrDie().getChildren()) {
            if (child != null) {
                final HtmlUnitScriptable scriptable = child.getScriptableObject();
                if (scriptable instanceof Element) {
                    counter++;
                }
            }
        }
        return counter;
    }

    /**
     * Returns the first element child.
     * @return the first element child
     */
    protected Element getFirstElementChild() {
        final DomNode domNode = getDomNodeOrDie();
        if (domNode instanceof DomElement) {
            final DomElement child = ((DomElement) domNode).getFirstElementChild();
            if (child != null) {
                return child.getScriptableObject();
            }
            return null;
        }

        for (final DomNode child : domNode.getChildren()) {
            if (child != null) {
                final HtmlUnitScriptable scriptable = child.getScriptableObject();
                if (scriptable instanceof Element) {
                    return (Element) scriptable;
                }
            }
        }
        return null;
    }

    /**
     * Returns the last element child.
     * @return the last element child
     */
    protected Element getLastElementChild() {
        final DomNode domNode = getDomNodeOrDie();
        if (domNode instanceof DomElement) {
            final DomElement child = ((DomElement) getDomNodeOrDie()).getLastElementChild();
            if (child != null) {
                return child.getScriptableObject();
            }
            return null;
        }

        Element result = null;
        for (final DomNode child : domNode.getChildren()) {
            final HtmlUnitScriptable scriptable = child.getScriptableObject();
            if (scriptable instanceof Element) {
                result = (Element) scriptable;
            }
        }
        return result;
    }

    /**
     * Gets the children of the current node.
     * @see MSDN documentation
     * @return the child at the given position
     */
    protected HTMLCollection getChildren() {
        final DomNode node = getDomNodeOrDie();
        final HTMLCollection childrenColl = new HTMLCollection(node, false);
        childrenColl.setElementsSupplier(
                (Supplier> & Serializable)
                () -> {
                    final List children = new ArrayList<>();
                    for (final DomNode domNode : node.getChildNodes()) {
                        if (domNode instanceof DomElement) {
                            children.add(domNode);
                        }
                    }
                    return children;
                });
        return childrenColl;
    }

    /**
     * Inserts a set of Node or DOMString objects in the children list of this ChildNode's parent,
     * just after this ChildNode.
     * @param context the context
     * @param thisObj this object
     * @param args the arguments
     * @param function the function
     */
    protected static void after(final Context context, final Scriptable thisObj, final Object[] args,
            final Function function) {
        final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
        final DomNode parentNode = thisDomNode.getParentNode();
        final DomNode nextSibling = thisDomNode.getNextSibling();
        for (final Object arg : args) {
            final Node node = toNodeOrTextNode((Node) thisObj, arg);
            final DomNode newNode = node.getDomNodeOrDie();
            if (nextSibling == null) {
                parentNode.appendChild(newNode);
            }
            else {
                nextSibling.insertBefore(newNode);
            }
        }
    }

    /**
     * Inserts a set of Node objects or string objects after the last child of the Element.
     * String objects are inserted as equivalent Text nodes.
     * @param context the context
     * @param thisObj this object
     * @param args the arguments
     * @param function the function
     */
    protected static void append(final Context context, final Scriptable thisObj, final Object[] args,
            final Function function) {
        if (!(thisObj instanceof Element)) {
            throw JavaScriptEngine.typeError("Illegal invocation");
        }

        final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();

        for (final Object arg : args) {
            final Node node = toNodeOrTextNode((Node) thisObj, arg);
            final DomNode newNode = node.getDomNodeOrDie();
            thisDomNode.appendChild(newNode);
        }
    }

    /**
     * Inserts a set of Node objects or string objects before the first child of the Element.
     * String objects are inserted as equivalent Text nodes.
     * @param context the context
     * @param thisObj this object
     * @param args the arguments
     * @param function the function
     */
    protected static void prepend(final Context context, final Scriptable thisObj, final Object[] args,
            final Function function) {
        if (!(thisObj instanceof Element)) {
            throw JavaScriptEngine.typeError("Illegal invocation");
        }

        final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
        final DomNode firstChild = thisDomNode.getFirstChild();
        for (final Object arg : args) {
            final Node node = toNodeOrTextNode((Node) thisObj, arg);
            final DomNode newNode = node.getDomNodeOrDie();
            if (firstChild == null) {
                thisDomNode.appendChild(newNode);
            }
            else {
                firstChild.insertBefore(newNode);
            }
        }
    }

    /**
     * Replaces the existing children of a Node with a specified new set of children.
     * These can be string or Node objects.
     * @param context the context
     * @param thisObj this object
     * @param args the arguments
     * @param function the function
     */
    protected static void replaceChildren(final Context context, final Scriptable thisObj, final Object[] args,
            final Function function) {
        if (!(thisObj instanceof Element)) {
            throw JavaScriptEngine.typeError("Illegal invocation");
        }

        final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
        thisDomNode.removeAllChildren();

        for (final Object arg : args) {
            final Node node = toNodeOrTextNode((Node) thisObj, arg);
            thisDomNode.appendChild(node.getDomNodeOrDie());
        }
    }

    private static Node toNodeOrTextNode(final Node thisObj, final Object obj) {
        if (obj instanceof Node) {
            return (Node) obj;
        }
        return (Node)
                ((HTMLDocument) thisObj.getOwnerDocument()).createTextNode(JavaScriptEngine.toString(obj));
    }

    /**
     * Inserts a set of Node or DOMString objects in the children list of this ChildNode's parent,
     * just before this ChildNode.
     * @param context the context
     * @param thisObj this object
     * @param args the arguments
     * @param function the function
     */
    protected static void before(final Context context, final Scriptable thisObj, final Object[] args,
            final Function function) {
        for (final Object arg : args) {
            final Node node = toNodeOrTextNode((Node) thisObj, arg);
            ((Node) thisObj).getDomNodeOrDie().insertBefore(node.getDomNodeOrDie());
        }
    }

    /**
     * Replaces this ChildNode in the children list of its parent with a set of Node or DOMString objects.
     * @param context the context
     * @param thisObj this object
     * @param args the arguments
     * @param function the function
     */
    protected static void replaceWith(final Context context, final Scriptable thisObj, final Object[] args,
            final Function function) {
        final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
        final DomNode parentNode = thisDomNode.getParentNode();

        if (args.length == 0) {
            parentNode.removeChild(thisDomNode);
            return;
        }

        final DomNode nextSibling = thisDomNode.getNextSibling();
        boolean isFirst = true;
        for (final Object arg : args) {
            final DomNode newNode = toNodeOrTextNode((Node) thisObj, arg).getDomNodeOrDie();
            if (isFirst) {
                isFirst = false;
                thisDomNode.replace(newNode);
            }
            else {
                if (nextSibling == null) {
                    parentNode.appendChild(newNode);
                }
                else {
                    nextSibling.insertBefore(newNode);
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy