View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.dom;
16  
17  import java.io.Serializable;
18  import java.util.ArrayList;
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Objects;
23  import java.util.function.Supplier;
24  
25  import org.htmlunit.SgmlPage;
26  import org.htmlunit.corejs.javascript.Context;
27  import org.htmlunit.corejs.javascript.Function;
28  import org.htmlunit.corejs.javascript.Scriptable;
29  import org.htmlunit.html.DomDocumentFragment;
30  import org.htmlunit.html.DomElement;
31  import org.htmlunit.html.DomNode;
32  import org.htmlunit.html.HtmlElement;
33  import org.htmlunit.html.HtmlInlineFrame;
34  import org.htmlunit.javascript.HtmlUnitScriptable;
35  import org.htmlunit.javascript.JavaScriptEngine;
36  import org.htmlunit.javascript.configuration.JsxClass;
37  import org.htmlunit.javascript.configuration.JsxConstant;
38  import org.htmlunit.javascript.configuration.JsxConstructor;
39  import org.htmlunit.javascript.configuration.JsxFunction;
40  import org.htmlunit.javascript.configuration.JsxGetter;
41  import org.htmlunit.javascript.configuration.JsxSetter;
42  import org.htmlunit.javascript.host.Element;
43  import org.htmlunit.javascript.host.NamedNodeMap;
44  import org.htmlunit.javascript.host.event.EventTarget;
45  import org.htmlunit.javascript.host.html.HTMLCollection;
46  import org.htmlunit.javascript.host.html.HTMLDocument;
47  import org.htmlunit.javascript.host.html.HTMLHtmlElement;
48  
49  /**
50   * The JavaScript object {@code Node} which is the base class for all DOM
51   * objects. This will typically wrap an instance of {@link DomNode}.
52   *
53   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
54   * @author David K. Taylor
55   * @author Barnaby Court
56   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
57   * @author <a href="mailto:george@murnock.com">George Murnock</a>
58   * @author Chris Erskine
59   * @author Bruce Faulkner
60   * @author Ahmed Ashour
61   * @author Ronald Brill
62   * @author Frank Danek
63   */
64  @JsxClass
65  public class Node extends EventTarget {
66  
67      /**
68       * @see org.w3c.dom.Node#ELEMENT_NODE
69       */
70      @JsxConstant
71      public static final int ELEMENT_NODE = org.w3c.dom.Node.ELEMENT_NODE;
72  
73      /**
74       * @see org.w3c.dom.Node#ATTRIBUTE_NODE
75       */
76      @JsxConstant
77      public static final int ATTRIBUTE_NODE = org.w3c.dom.Node.ATTRIBUTE_NODE;
78  
79      /**
80       * @see org.w3c.dom.Node#TEXT_NODE
81       */
82      @JsxConstant
83      public static final int TEXT_NODE = org.w3c.dom.Node.TEXT_NODE;
84  
85      /**
86       * @see org.w3c.dom.Node#CDATA_SECTION_NODE
87       */
88      @JsxConstant
89      public static final int CDATA_SECTION_NODE = org.w3c.dom.Node.CDATA_SECTION_NODE;
90  
91      /**
92       * @see org.w3c.dom.Node#ENTITY_REFERENCE_NODE
93       */
94      @JsxConstant
95      public static final int ENTITY_REFERENCE_NODE = org.w3c.dom.Node.ENTITY_REFERENCE_NODE;
96  
97      /**
98       * @see org.w3c.dom.Node#ENTITY_NODE
99       */
100     @JsxConstant
101     public static final int ENTITY_NODE = org.w3c.dom.Node.ENTITY_NODE;
102 
103     /**
104      * @see org.w3c.dom.Node#PROCESSING_INSTRUCTION_NODE
105      */
106     @JsxConstant
107     public static final int PROCESSING_INSTRUCTION_NODE = org.w3c.dom.Node.PROCESSING_INSTRUCTION_NODE;
108 
109     /**
110      * @see org.w3c.dom.Node#COMMENT_NODE
111      */
112     @JsxConstant
113     public static final int COMMENT_NODE = org.w3c.dom.Node.COMMENT_NODE;
114 
115     /**
116      * @see org.w3c.dom.Node#DOCUMENT_NODE
117      */
118     @JsxConstant
119     public static final int DOCUMENT_NODE = org.w3c.dom.Node.DOCUMENT_NODE;
120 
121     /**
122      * @see org.w3c.dom.Node#DOCUMENT_TYPE_NODE
123      */
124     @JsxConstant
125     public static final int DOCUMENT_TYPE_NODE = org.w3c.dom.Node.DOCUMENT_TYPE_NODE;
126 
127     /**
128      * @see org.w3c.dom.Node#DOCUMENT_FRAGMENT_NODE
129      */
130     @JsxConstant
131     public static final int DOCUMENT_FRAGMENT_NODE = org.w3c.dom.Node.DOCUMENT_FRAGMENT_NODE;
132 
133     /**
134      * @see org.w3c.dom.Node#NOTATION_NODE
135      */
136     @JsxConstant
137     public static final int NOTATION_NODE = org.w3c.dom.Node.NOTATION_NODE;
138 
139     /**
140      * @see org.w3c.dom.Node#DOCUMENT_POSITION_DISCONNECTED
141      */
142     @JsxConstant
143     public static final int DOCUMENT_POSITION_DISCONNECTED = org.w3c.dom.Node.DOCUMENT_POSITION_DISCONNECTED;
144 
145     /**
146      * @see org.w3c.dom.Node#DOCUMENT_POSITION_PRECEDING
147      */
148     @JsxConstant
149     public static final int DOCUMENT_POSITION_PRECEDING = org.w3c.dom.Node.DOCUMENT_POSITION_PRECEDING;
150 
151     /**
152      * @see org.w3c.dom.Node#DOCUMENT_POSITION_FOLLOWING
153      */
154     @JsxConstant
155     public static final int DOCUMENT_POSITION_FOLLOWING = org.w3c.dom.Node.DOCUMENT_POSITION_FOLLOWING;
156 
157     /**
158      * @see org.w3c.dom.Node#DOCUMENT_POSITION_CONTAINS
159      */
160     @JsxConstant
161     public static final int DOCUMENT_POSITION_CONTAINS = org.w3c.dom.Node.DOCUMENT_POSITION_CONTAINS;
162 
163     /**
164      * @see org.w3c.dom.Node#DOCUMENT_POSITION_CONTAINED_BY
165      */
166     @JsxConstant
167     public static final int DOCUMENT_POSITION_CONTAINED_BY = org.w3c.dom.Node.DOCUMENT_POSITION_CONTAINED_BY;
168 
169     /**
170      * @see org.w3c.dom.Node#DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
171      */
172     @JsxConstant
173     public static final int DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
174         = org.w3c.dom.Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
175 
176     /** "Live" child nodes collection; has to be a member to have equality (==) working. */
177     private NodeList childNodes_;
178 
179     /**
180      * JavaScript constructor.
181      */
182     @Override
183     @JsxConstructor
184     public void jsConstructor() {
185         super.jsConstructor();
186     }
187 
188     /**
189      * Gets the JavaScript property {@code nodeType} for the current node.
190      * @return the node type
191      */
192     @JsxGetter
193     public int getNodeType() {
194         return getDomNodeOrDie().getNodeType();
195     }
196 
197     /**
198      * Gets the JavaScript property {@code nodeName} for the current node.
199      * @return the node name
200      */
201     @JsxGetter
202     public String getNodeName() {
203         return getDomNodeOrDie().getNodeName();
204     }
205 
206     /**
207      * Gets the JavaScript property {@code nodeValue} for the current node.
208      * @return the node value
209      */
210     @JsxGetter
211     public String getNodeValue() {
212         return getDomNodeOrDie().getNodeValue();
213     }
214 
215     /**
216      * Sets the JavaScript property {@code nodeValue} for the current node.
217      * @param newValue the new node value
218      */
219     @JsxSetter
220     public void setNodeValue(final String newValue) {
221         getDomNodeOrDie().setNodeValue(newValue);
222     }
223 
224     /**
225      * Adds a DOM node to the node.
226      * @param childObject the node to add to this node
227      * @return the newly added child node
228      */
229     @JsxFunction
230     public Node appendChild(final Object childObject) {
231         if (childObject instanceof Node) {
232             final Node childNode = (Node) childObject;
233 
234             // is the node allowed here?
235             if (!isNodeInsertable(childNode)) {
236                 throw JavaScriptEngine.asJavaScriptException(
237                         getWindow(),
238                         "Node cannot be inserted at the specified point in the hierarchy",
239                         DOMException.HIERARCHY_REQUEST_ERR);
240             }
241 
242             // Get XML node for the DOM node passed in
243             final DomNode childDomNode = childNode.getDomNodeOrDie();
244 
245             // Get the parent XML node that the child should be added to.
246             final DomNode parentNode = getDomNodeOrDie();
247 
248             // Append the child to the parent node
249             try {
250                 parentNode.appendChild(childDomNode);
251             }
252             catch (final org.w3c.dom.DOMException e) {
253                 throw JavaScriptEngine.asJavaScriptException(getWindow(), e.getMessage(), e.code);
254             }
255 
256             initInlineFrameIfNeeded(childDomNode);
257             for (final HtmlElement htmlElement : childDomNode.getHtmlElementDescendants()) {
258                 initInlineFrameIfNeeded(htmlElement);
259             }
260             return childNode;
261         }
262         return null;
263     }
264 
265     /**
266      * If we have added a new iframe that
267      * had no source attribute, we have to take care the
268      * 'onload' handler is triggered.
269      */
270     private static void initInlineFrameIfNeeded(final DomNode childDomNode) {
271         if (childDomNode instanceof HtmlInlineFrame) {
272             final HtmlInlineFrame frame = (HtmlInlineFrame) childDomNode;
273             if (DomElement.ATTRIBUTE_NOT_DEFINED == frame.getSrcAttribute()) {
274                 frame.loadInnerPage();
275             }
276         }
277     }
278 
279     /**
280      * Add a DOM node as a child to this node before the referenced node.
281      * If the referenced node is null, append to the end.
282      * @param context the JavaScript context
283      * @param scope the scope
284      * @param thisObj the scriptable
285      * @param args the arguments passed into the method
286      * @param function the function
287      * @return the newly added child node
288      */
289     @JsxFunction
290     public static Node insertBefore(final Context context, final Scriptable scope,
291             final Scriptable thisObj, final Object[] args, final Function function) {
292         return ((Node) thisObj).insertBeforeImpl(args);
293     }
294 
295     /**
296      * Add a DOM node as a child to this node before the referenced node.
297      * If the referenced node is null, append to the end.
298      * @param args the arguments
299      * @return the newly added child node
300      */
301     protected Node insertBeforeImpl(final Object[] args) {
302         if (args.length < 1) {
303             throw JavaScriptEngine.typeError(
304                     "Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only 0 present.");
305         }
306 
307         final Object newChildObject = args[0];
308         final Object refChildObject;
309         if (args.length > 1) {
310             refChildObject = args[1];
311         }
312         else {
313             refChildObject = JavaScriptEngine.UNDEFINED;
314         }
315 
316         if (newChildObject instanceof Node) {
317             final Node newChild = (Node) newChildObject;
318 
319             // is the node allowed here?
320             if (!isNodeInsertable(newChild)) {
321                 throw JavaScriptEngine.asJavaScriptException(
322                         getWindow(),
323                         "Node cannot be inserted at the specified point in the hierarchy",
324                         DOMException.HIERARCHY_REQUEST_ERR);
325             }
326 
327             final DomNode newChildNode = newChild.getDomNodeOrDie();
328             if (newChildNode instanceof DomDocumentFragment) {
329                 final DomDocumentFragment fragment = (DomDocumentFragment) newChildNode;
330                 for (final DomNode child : fragment.getChildren()) {
331                     if (!isNodeInsertable(child.getScriptableObject())) {
332                         throw JavaScriptEngine.asJavaScriptException(
333                                 getWindow(),
334                                 "Node cannot be inserted at the specified point in the hierarchy",
335                                 DOMException.HIERARCHY_REQUEST_ERR);
336                     }
337                 }
338             }
339 
340             // extract refChild
341             final DomNode refChildNode;
342             if (JavaScriptEngine.isUndefined(refChildObject)) {
343                 if (args.length == 2) {
344                     refChildNode = null;
345                 }
346                 else {
347                     throw JavaScriptEngine.typeError(
348                             "Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only 1 present.");
349                 }
350             }
351             else if (refChildObject == null) {
352                 refChildNode = null;
353             }
354             else {
355                 refChildNode = ((Node) refChildObject).getDomNodeOrDie();
356             }
357 
358             final DomNode domNode = getDomNodeOrDie();
359 
360             try {
361                 domNode.insertBefore(newChildNode, refChildNode);
362             }
363             catch (final org.w3c.dom.DOMException e) {
364                 throw JavaScriptEngine.asJavaScriptException(getWindow(), e.getMessage(), DOMException.NOT_FOUND_ERR);
365             }
366             return newChild;
367         }
368         return null;
369     }
370 
371     /**
372      * Indicates if the node can be inserted.
373      * @param childObject the node
374      * @return {@code false} if it is not allowed here
375      */
376     private static boolean isNodeInsertable(final Node childObject) {
377         if (childObject instanceof HTMLHtmlElement) {
378             final DomNode domNode = childObject.getDomNodeOrDie();
379             return domNode.getPage().getDocumentElement() != domNode;
380         }
381         return true;
382     }
383 
384     /**
385      * Removes the DOM node from its parent.
386      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove">MDN documentation</a>
387      */
388     protected void remove() {
389         getDomNodeOrDie().remove();
390     }
391 
392     /**
393      * Removes a DOM node from this node.
394      * @param childObject the node to remove from this node
395      * @return the removed child node
396      */
397     @JsxFunction
398     public Node removeChild(final Object childObject) {
399         if (!(childObject instanceof Node)) {
400             return null;
401         }
402 
403         // Get XML node for the DOM node passed in
404         final Node childObjectNode = (Node) childObject;
405         final DomNode childDomNode = childObjectNode.getDomNodeOrDie();
406 
407         if (!getDomNodeOrDie().isAncestorOf(childDomNode)) {
408             throw JavaScriptEngine.asJavaScriptException(
409                     getWindow(),
410                     "Failed to execute 'removeChild' on '"
411                             + this + "': The node to be removed is not a child of this node.",
412                     DOMException.NOT_FOUND_ERR);
413         }
414         // Remove the child from the parent node
415         childDomNode.remove();
416         return childObjectNode;
417     }
418 
419     /**
420      * Replaces a child DOM node with another DOM node.
421      * @param newChildObject the node to add as a child of this node
422      * @param oldChildObject the node to remove as a child of this node
423      * @return the removed child node
424      */
425     @JsxFunction
426     public Node replaceChild(final Object newChildObject, final Object oldChildObject) {
427         if (newChildObject instanceof DocumentFragment) {
428             final DocumentFragment fragment = (DocumentFragment) newChildObject;
429             Node firstNode = null;
430 
431             final Node oldChildNode = (Node) oldChildObject;
432             final Node refChildObject = oldChildNode.getNextSibling();
433             for (final DomNode node : fragment.getDomNodeOrDie().getChildren()) {
434                 if (firstNode == null) {
435                     replaceChild(node.getScriptableObject(), oldChildObject);
436                     firstNode = node.getScriptableObject();
437                 }
438                 else {
439                     insertBeforeImpl(new Object[] {node.getScriptableObject(), refChildObject});
440                 }
441             }
442             if (firstNode == null) {
443                 removeChild(oldChildObject);
444             }
445 
446             return oldChildNode;
447         }
448 
449         if (newChildObject instanceof Node && oldChildObject instanceof Node) {
450             final Node newChild = (Node) newChildObject;
451 
452             // is the node allowed here?
453             if (!isNodeInsertable(newChild)) {
454                 throw JavaScriptEngine.asJavaScriptException(
455                         getWindow(),
456                         "Node cannot be inserted at the specified point in the hierarchy",
457                         DOMException.HIERARCHY_REQUEST_ERR);
458             }
459 
460             // Get XML nodes for the DOM nodes passed in
461             final DomNode newChildDomNode = newChild.getDomNodeOrDie();
462             final Node oldChildNode = (Node) oldChildObject;
463             final DomNode oldChildDomNode = oldChildNode.getDomNodeOrDie();
464 
465             // Replace the old child with the new child.
466             oldChildDomNode.replace(newChildDomNode);
467 
468             return oldChildNode;
469         }
470 
471         return null;
472     }
473 
474     /**
475      * Clones this node.
476      * @param deep if {@code true}, recursively clones all descendants
477      * @return the newly cloned node
478      */
479     @JsxFunction
480     public Node cloneNode(final boolean deep) {
481         final DomNode domNode = getDomNodeOrDie();
482         final DomNode clonedNode = domNode.cloneNode(deep);
483 
484         return getJavaScriptNode(clonedNode);
485     }
486 
487     /**
488      * Check if 2 nodes are equals.
489      * For detail specifications
490      * @see <a href="https://dom.spec.whatwg.org/#concept-node-equals">concept-node-equals</a>
491      * @param other the node to compare with
492      * @return true or false
493      */
494     @JsxFunction
495     public boolean isEqualNode(final Node other) {
496         if (isSameNode(other)) {
497             return true;
498         }
499 
500         if (other == null) {
501             return false;
502         }
503 
504         if (!getClassName().equals(other.getClassName())) {
505             return false;
506         }
507 
508         if (this instanceof DocumentType) {
509             final DocumentType docType = (DocumentType) this;
510             final DocumentType otherDocType = (DocumentType) other;
511             if (!Objects.equals(docType.getName(), otherDocType.getName())
512                     || !Objects.equals(docType.getPublicId(), otherDocType.getPublicId())
513                     || !Objects.equals(docType.getSystemId(), otherDocType.getSystemId())) {
514                 return false;
515             }
516 
517         }
518         else if (this instanceof Element) {
519             final Element element = (Element) this;
520             final Element otherElement = (Element) other;
521             if (!Objects.equals(element.getNodeName(), otherElement.getNodeName())
522                     || !Objects.equals(element.getPrefix(), otherElement.getPrefix())
523                     || !Objects.equals(element.getLocalName(), otherElement.getLocalName())) {
524                 return false;
525             }
526 
527             final NamedNodeMap attributesMap = element.getAttributes();
528             final NamedNodeMap otherAttributesMap = otherElement.getAttributes();
529             if (attributesMap != null || otherAttributesMap != null) {
530                 if (attributesMap == null || otherAttributesMap == null) {
531                     return false;
532                 }
533 
534                 final int length = attributesMap.getLength();
535                 if (length != otherAttributesMap.getLength()) {
536                     return false;
537                 }
538 
539                 final Map<String, Attr> name2Attributes = new HashMap<>();
540                 for (int i = 0; i < length; i++) {
541                     final Attr attribute = (Attr) attributesMap.item(i);
542                     name2Attributes.put(attribute.getName(), attribute);
543                 }
544 
545                 for (int i = 0; i < length; i++) {
546                     final Attr otherAttribute = (Attr) otherAttributesMap.item(i);
547                     final Attr attribute = name2Attributes.get(otherAttribute.getName());
548                     if (attribute == null) {
549                         return false;
550                     }
551                     if (!attribute.isEqualNode(otherAttribute)) {
552                         return false;
553                     }
554                 }
555             }
556 
557         }
558         else if (this instanceof Attr) {
559             final Attr attr = (Attr) this;
560             final Attr otherAttr = (Attr) other;
561             if (!Objects.equals(attr.getName(), otherAttr.getName())
562                     || !Objects.equals(attr.getLocalName(), otherAttr.getLocalName())
563                     || !Objects.equals(attr.getValue(), otherAttr.getValue())) {
564                 return false;
565             }
566 
567         }
568         else if (this instanceof ProcessingInstruction) {
569             final ProcessingInstruction instruction = (ProcessingInstruction) this;
570             final ProcessingInstruction otherInstruction = (ProcessingInstruction) other;
571             if (!Objects.equals(instruction.getTarget(), otherInstruction.getTarget())
572                     || !Objects.equals(instruction.getData(), otherInstruction.getData())) {
573                 return false;
574             }
575 
576         }
577         else if (this instanceof Text || this instanceof Comment) {
578             final CharacterData data = (CharacterData) this;
579             final CharacterData otherData = (CharacterData) other;
580             if (!Objects.equals(data.getData(), otherData.getData())) {
581                 return false;
582             }
583         }
584 
585         final NodeList childNodes = getChildNodes();
586         final NodeList otherChildNodes = other.getChildNodes();
587         if (childNodes != null || otherChildNodes != null) {
588             if (childNodes == null || otherChildNodes == null) {
589                 return false;
590             }
591 
592             final int length = childNodes.getLength();
593             final int otherLength = otherChildNodes.getLength();
594             if (length != otherLength) {
595                 return false;
596             }
597 
598             for (int i = 0; i < length; i++) {
599                 final Node childNode = (Node) childNodes.item(i);
600                 final Node otherChildNode = (Node) otherChildNodes.item(i);
601                 if (!childNode.isEqualNode(otherChildNode)) {
602                     return false;
603                 }
604             }
605         }
606 
607         return true;
608     }
609 
610     /**
611      * This method provides a way to determine whether two Node references returned by
612      * the implementation reference the same object.
613      * When two Node references are references to the same object, even if through a proxy,
614      * the references may be used completely interchangeably, such that all attributes
615      * have the same values and calling the same DOM method on either reference always has exactly the same effect.
616      *
617      * @param other the node to test against
618      *
619      * @return whether this node is the same node as the given one
620      */
621     @JsxFunction
622     public boolean isSameNode(final Object other) {
623         return this == other;
624     }
625 
626     /**
627      * Returns whether this node has any children.
628      * @return boolean true if this node has any children, false otherwise
629      */
630     @JsxFunction
631     public boolean hasChildNodes() {
632         return getDomNodeOrDie().getChildren().iterator().hasNext();
633     }
634 
635     /**
636      * @param namespace string containing the namespace to look the prefix up
637      * @return a string containing the prefix for a given namespace URI,
638      *         if present, and null if not. When multiple prefixes are possible,
639      *         the first prefix is returned.
640      */
641     @JsxFunction
642     public String lookupPrefix(final String namespace) {
643         return null;
644     }
645 
646     /**
647      * Returns the child nodes of the current element.
648      * @return the child nodes of the current element
649      */
650     @JsxGetter
651     public NodeList getChildNodes() {
652         if (childNodes_ == null) {
653             final DomNode node = getDomNodeOrDie();
654             childNodes_ = new NodeList(node, false);
655             childNodes_.setElementsSupplier(
656                     (Supplier<List<DomNode>> & Serializable)
657                     () -> {
658                         final List<DomNode> response = new ArrayList<>();
659                         for (final DomNode child : node.getChildren()) {
660                             response.add(child);
661                         }
662 
663                         return response;
664                     });
665         }
666         return childNodes_;
667     }
668 
669     /**
670      * Returns this node's parent node.
671      * @return this node's parent node
672      */
673     public final Node getParent() {
674         return getJavaScriptNode(getDomNodeOrDie().getParentNode());
675     }
676 
677     /**
678      * Gets the JavaScript property {@code parentNode} for the node that
679      * contains the current node.
680      * @return the parent node
681      */
682     @JsxGetter
683     public Object getParentNode() {
684         return getJavaScriptNode(getDomNodeOrDie().getParentNode());
685     }
686 
687     /**
688      * Gets the JavaScript property {@code nextSibling} for the node that
689      * contains the current node.
690      * @return the next sibling node or null if the current node has
691      *         no next sibling.
692      */
693     @JsxGetter
694     public Node getNextSibling() {
695         return getJavaScriptNode(getDomNodeOrDie().getNextSibling());
696     }
697 
698     /**
699      * Gets the JavaScript property {@code previousSibling} for the node that
700      * contains the current node.
701      * @return the previous sibling node or null if the current node has
702      *         no previous sibling.
703      */
704     @JsxGetter
705     public Node getPreviousSibling() {
706         return getJavaScriptNode(getDomNodeOrDie().getPreviousSibling());
707     }
708 
709     /**
710      * Gets the JavaScript property {@code firstChild} for the node that
711      * contains the current node.
712      * @return the first child node or null if the current node has
713      *         no children.
714      */
715     @JsxGetter
716     public Node getFirstChild() {
717         return getJavaScriptNode(getDomNodeOrDie().getFirstChild());
718     }
719 
720     /**
721      * Gets the JavaScript property {@code lastChild} for the node that
722      * contains the current node.
723      * @return the last child node or null if the current node has
724      *         no children.
725      */
726     @JsxGetter
727     public Node getLastChild() {
728         return getJavaScriptNode(getDomNodeOrDie().getLastChild());
729     }
730 
731     /**
732      * Gets the JavaScript node for a given DomNode.
733      * @param domNode the DomNode
734      * @return the JavaScript node or null if the DomNode was null
735      */
736     protected Node getJavaScriptNode(final DomNode domNode) {
737         if (domNode == null) {
738             return null;
739         }
740         return (Node) getScriptableFor(domNode);
741     }
742 
743     /**
744      * Returns the owner document.
745      * @return the document
746      */
747     @JsxGetter
748     public HtmlUnitScriptable getOwnerDocument() {
749         final Object document = getDomNodeOrDie().getOwnerDocument();
750         if (document != null) {
751             return ((SgmlPage) document).getScriptableObject();
752         }
753         return null;
754     }
755 
756     /**
757      * Returns the owner document.
758      * @return the document
759      */
760     @JsxFunction
761     public Node getRootNode() {
762         Node parent = this;
763         while (parent != null) {
764             if (parent instanceof Document || parent instanceof DocumentFragment) {
765                 return parent;
766             }
767             parent = parent.getParent();
768         }
769         return this;
770     }
771 
772     /**
773      * Compares the positions of this node and the provided node within the document.
774      * @param node node object that specifies the node to check
775      * @return how the node is positioned relatively to the reference node.
776      * @see <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition">DOM level 3</a>
777      * @see org.w3c.dom.Node#compareDocumentPosition(org.w3c.dom.Node)
778      */
779     @JsxFunction
780     public int compareDocumentPosition(final Object node) {
781         if (!(node instanceof Node)) {
782             throw JavaScriptEngine.typeError("Could not convert JavaScript argument arg 0");
783         }
784         return getDomNodeOrDie().compareDocumentPosition(((Node) node).getDomNodeOrDie());
785     }
786 
787     /**
788      * Merges adjacent TextNode objects to produce a normalized document object model.
789      */
790     @JsxFunction
791     public void normalize() {
792         getDomNodeOrDie().normalize();
793     }
794 
795     /**
796      * Gets the textContent attribute.
797      * @return the contents of this node as text
798      */
799     @JsxGetter
800     public String getTextContent() {
801         return getDomNodeOrDie().getTextContent();
802     }
803 
804     /**
805      * Replace all children elements of this element with the supplied value.
806      * @param value - the new value for the contents of this node
807      */
808     @JsxSetter
809     public void setTextContent(final Object value) {
810         getDomNodeOrDie().setTextContent(value == null ? null : JavaScriptEngine.toString(value));
811     }
812 
813     /**
814      * Gets the JavaScript property {@code parentElement}.
815      * @return the parent element
816      * @see #getParentNode()
817      */
818     @JsxGetter
819     public Element getParentElement() {
820         final Node parent = getParent();
821         if (!(parent instanceof Element)) {
822             return null;
823         }
824         return (Element) parent;
825     }
826 
827     /**
828      * Returns the attributes of this XML element.
829      * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/Node.attributes">Gecko DOM Reference</a>
830      * @return the attributes of this XML element
831      */
832     public NamedNodeMap getAttributes() {
833         return null;
834     }
835 
836     /**
837      * Checks whether the given element is contained within this object.
838      * @param element element object that specifies the element to check
839      * @return true if the element is contained within this object
840      */
841     @JsxFunction
842     public boolean contains(final Object element) {
843         if (element == null || JavaScriptEngine.isUndefined(element)) {
844             return false;
845         }
846 
847         if (!(element instanceof Node)) {
848             throw JavaScriptEngine.reportRuntimeError("Could not convert JavaScript argument arg 0");
849         }
850 
851         for (Node parent = (Node) element; parent != null; parent = parent.getParentElement()) {
852             if (this == parent) {
853                 return true;
854             }
855         }
856         return false;
857     }
858 
859     /**
860      * Returns the Base URI as a string.
861      * @return the Base URI as a string
862      */
863     @JsxGetter
864     public String getBaseURI() {
865         return getDomNodeOrDie().getBaseURI();
866     }
867 
868     /**
869      * Returns true when the current element has any attributes or not.
870      * @return true if an attribute is specified on this element
871      */
872     public boolean hasAttributes() {
873         return getDomNodeOrDie().hasAttributes();
874     }
875 
876     /**
877      * Returns the namespace prefix.
878      * @return the namespace prefix
879      */
880     public String getPrefix() {
881         return getDomNodeOrDie().getPrefix();
882     }
883 
884     /**
885      * Returns the local name of this attribute.
886      * @return the local name of this attribute
887      */
888     public String getLocalName() {
889         return getDomNodeOrDie().getLocalName();
890     }
891 
892     /**
893      * Returns the URI that identifies an XML namespace.
894      * @return the URI that identifies an XML namespace
895      */
896     public String getNamespaceURI() {
897         return getDomNodeOrDie().getNamespaceURI();
898     }
899 
900     /**
901      * Returns the current number of child elements.
902      * @return the child element count
903      */
904     protected int getChildElementCount() {
905         final DomNode domNode = getDomNodeOrDie();
906         if (domNode instanceof DomElement) {
907             return ((DomElement) domNode).getChildElementCount();
908         }
909 
910         int counter = 0;
911         for (final DomNode child : getDomNodeOrDie().getChildren()) {
912             if (child != null) {
913                 final HtmlUnitScriptable scriptable = child.getScriptableObject();
914                 if (scriptable instanceof Element) {
915                     counter++;
916                 }
917             }
918         }
919         return counter;
920     }
921 
922     /**
923      * Returns the first element child.
924      * @return the first element child
925      */
926     protected Element getFirstElementChild() {
927         final DomNode domNode = getDomNodeOrDie();
928         if (domNode instanceof DomElement) {
929             final DomElement child = ((DomElement) domNode).getFirstElementChild();
930             if (child != null) {
931                 return child.getScriptableObject();
932             }
933             return null;
934         }
935 
936         for (final DomNode child : domNode.getChildren()) {
937             if (child != null) {
938                 final HtmlUnitScriptable scriptable = child.getScriptableObject();
939                 if (scriptable instanceof Element) {
940                     return (Element) scriptable;
941                 }
942             }
943         }
944         return null;
945     }
946 
947     /**
948      * Returns the last element child.
949      * @return the last element child
950      */
951     protected Element getLastElementChild() {
952         final DomNode domNode = getDomNodeOrDie();
953         if (domNode instanceof DomElement) {
954             final DomElement child = ((DomElement) getDomNodeOrDie()).getLastElementChild();
955             if (child != null) {
956                 return child.getScriptableObject();
957             }
958             return null;
959         }
960 
961         Element result = null;
962         for (final DomNode child : domNode.getChildren()) {
963             final HtmlUnitScriptable scriptable = child.getScriptableObject();
964             if (scriptable instanceof Element) {
965                 result = (Element) scriptable;
966             }
967         }
968         return result;
969     }
970 
971     /**
972      * Gets the children of the current node.
973      * @see <a href="http://msdn.microsoft.com/en-us/library/ms537446.aspx">MSDN documentation</a>
974      * @return the child at the given position
975      */
976     protected HTMLCollection getChildren() {
977         final DomNode node = getDomNodeOrDie();
978         final HTMLCollection childrenColl = new HTMLCollection(node, false);
979         childrenColl.setElementsSupplier(
980                 (Supplier<List<DomNode>> & Serializable)
981                 () -> {
982                     final List<DomNode> children = new ArrayList<>();
983                     for (final DomNode domNode : node.getChildNodes()) {
984                         if (domNode instanceof DomElement) {
985                             children.add(domNode);
986                         }
987                     }
988                     return children;
989                 });
990         return childrenColl;
991     }
992 
993     /**
994      * Inserts a set of Node or DOMString objects in the children list of this ChildNode's parent,
995      * just after this ChildNode.
996      * @param context the context
997      * @param thisObj this object
998      * @param args the arguments
999      * @param function the function
1000      */
1001     protected static void after(final Context context, final Scriptable thisObj, final Object[] args,
1002             final Function function) {
1003         final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
1004         final DomNode parentNode = thisDomNode.getParentNode();
1005         final DomNode nextSibling = thisDomNode.getNextSibling();
1006         for (final Object arg : args) {
1007             final Node node = toNodeOrTextNode((Node) thisObj, arg);
1008             final DomNode newNode = node.getDomNodeOrDie();
1009             if (nextSibling == null) {
1010                 parentNode.appendChild(newNode);
1011             }
1012             else {
1013                 nextSibling.insertBefore(newNode);
1014             }
1015         }
1016     }
1017 
1018     /**
1019      * Inserts a set of Node objects or string objects after the last child of the Element.
1020      * String objects are inserted as equivalent Text nodes.
1021      * @param context the context
1022      * @param thisObj this object
1023      * @param args the arguments
1024      * @param function the function
1025      */
1026     protected static void append(final Context context, final Scriptable thisObj, final Object[] args,
1027             final Function function) {
1028         if (!(thisObj instanceof Node)) {
1029             throw JavaScriptEngine.typeError("Illegal invocation");
1030         }
1031 
1032         final Node thisNode = (Node) thisObj;
1033         final DomNode thisDomNode = thisNode.getDomNodeOrDie();
1034 
1035         for (final Object arg : args) {
1036             final Node node = toNodeOrTextNode(thisNode, arg);
1037             thisDomNode.appendChild(node.getDomNodeOrDie());
1038         }
1039     }
1040 
1041     /**
1042      * Inserts a set of Node objects or string objects before the first child of the Element.
1043      * String objects are inserted as equivalent Text nodes.
1044      * @param context the context
1045      * @param thisObj this object
1046      * @param args the arguments
1047      * @param function the function
1048      */
1049     protected static void prepend(final Context context, final Scriptable thisObj, final Object[] args,
1050             final Function function) {
1051         if (!(thisObj instanceof Node)) {
1052             throw JavaScriptEngine.typeError("Illegal invocation");
1053         }
1054 
1055         final Node thisNode = (Node) thisObj;
1056         final DomNode thisDomNode = thisNode.getDomNodeOrDie();
1057         final DomNode firstChild = thisDomNode.getFirstChild();
1058 
1059         for (final Object arg : args) {
1060             final Node node = toNodeOrTextNode(thisNode, arg);
1061             final DomNode newNode = node.getDomNodeOrDie();
1062             if (firstChild == null) {
1063                 thisDomNode.appendChild(newNode);
1064             }
1065             else {
1066                 firstChild.insertBefore(newNode);
1067             }
1068         }
1069     }
1070 
1071     /**
1072      * Replaces the existing children of a Node with a specified new set of children.
1073      * These can be string or Node objects.
1074      * @param context the context
1075      * @param thisObj this object
1076      * @param args the arguments
1077      * @param function the function
1078      */
1079     protected static void replaceChildren(final Context context, final Scriptable thisObj, final Object[] args,
1080             final Function function) {
1081         if (!(thisObj instanceof Node)) {
1082             throw JavaScriptEngine.typeError("Illegal invocation");
1083         }
1084 
1085         final Node thisNode = (Node) thisObj;
1086         final DomNode thisDomNode = thisNode.getDomNodeOrDie();
1087         thisDomNode.removeAllChildren();
1088 
1089         for (final Object arg : args) {
1090             final Node node = toNodeOrTextNode(thisNode, arg);
1091             thisDomNode.appendChild(node.getDomNodeOrDie());
1092         }
1093     }
1094 
1095     private static Node toNodeOrTextNode(final Node thisObj, final Object obj) {
1096         if (obj instanceof Node) {
1097             return (Node) obj;
1098         }
1099         return (Node)
1100                 ((HTMLDocument) thisObj.getOwnerDocument()).createTextNode(JavaScriptEngine.toString(obj));
1101     }
1102 
1103     /**
1104      * Inserts a set of Node or DOMString objects in the children list of this ChildNode's parent,
1105      * just before this ChildNode.
1106      * @param context the context
1107      * @param thisObj this object
1108      * @param args the arguments
1109      * @param function the function
1110      */
1111     protected static void before(final Context context, final Scriptable thisObj, final Object[] args,
1112             final Function function) {
1113         for (final Object arg : args) {
1114             final Node node = toNodeOrTextNode((Node) thisObj, arg);
1115             ((Node) thisObj).getDomNodeOrDie().insertBefore(node.getDomNodeOrDie());
1116         }
1117     }
1118 
1119     /**
1120      * Replaces this ChildNode in the children list of its parent with a set of Node or DOMString objects.
1121      * @param context the context
1122      * @param thisObj this object
1123      * @param args the arguments
1124      * @param function the function
1125      */
1126     protected static void replaceWith(final Context context, final Scriptable thisObj, final Object[] args,
1127             final Function function) {
1128         final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
1129         final DomNode parentNode = thisDomNode.getParentNode();
1130 
1131         if (args.length == 0) {
1132             parentNode.removeChild(thisDomNode);
1133             return;
1134         }
1135 
1136         final DomNode nextSibling = thisDomNode.getNextSibling();
1137         boolean isFirst = true;
1138         for (final Object arg : args) {
1139             final DomNode newNode = toNodeOrTextNode((Node) thisObj, arg).getDomNodeOrDie();
1140             if (isFirst) {
1141                 isFirst = false;
1142                 thisDomNode.replace(newNode);
1143             }
1144             else {
1145                 if (nextSibling == null) {
1146                     parentNode.appendChild(newNode);
1147                 }
1148                 else {
1149                     nextSibling.insertBefore(newNode);
1150                 }
1151             }
1152         }
1153     }
1154 }