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 (!getClassName().equals(other.getClassName())) {
501             return false;
502         }
503 
504         if (this instanceof DocumentType) {
505             final DocumentType docType = (DocumentType) this;
506             final DocumentType otherDocType = (DocumentType) other;
507             if (!Objects.equals(docType.getName(), otherDocType.getName())
508                     || !Objects.equals(docType.getPublicId(), otherDocType.getPublicId())
509                     || !Objects.equals(docType.getSystemId(), otherDocType.getSystemId())) {
510                 return false;
511             }
512 
513         }
514         else if (this instanceof Element) {
515             final Element element = (Element) this;
516             final Element otherElement = (Element) other;
517             if (!Objects.equals(element.getNodeName(), otherElement.getNodeName())
518                     || !Objects.equals(element.getPrefix(), otherElement.getPrefix())
519                     || !Objects.equals(element.getLocalName(), otherElement.getLocalName())) {
520                 return false;
521             }
522 
523             final NamedNodeMap attributesMap = element.getAttributes();
524             final NamedNodeMap otherAttributesMap = otherElement.getAttributes();
525             if (attributesMap != null || otherAttributesMap != null) {
526                 if (attributesMap == null || otherAttributesMap == null) {
527                     return false;
528                 }
529 
530                 final int length = attributesMap.getLength();
531                 if (length != otherAttributesMap.getLength()) {
532                     return false;
533                 }
534 
535                 final Map<String, Attr> name2Attributes = new HashMap<>();
536                 for (int i = 0; i < length; i++) {
537                     final Attr attribute = (Attr) attributesMap.item(i);
538                     name2Attributes.put(attribute.getName(), attribute);
539                 }
540 
541                 for (int i = 0; i < length; i++) {
542                     final Attr otherAttribute = (Attr) otherAttributesMap.item(i);
543                     final Attr attribute = name2Attributes.get(otherAttribute.getName());
544                     if (attribute == null) {
545                         return false;
546                     }
547                     if (!attribute.isEqualNode(otherAttribute)) {
548                         return false;
549                     }
550                 }
551             }
552 
553         }
554         else if (this instanceof Attr) {
555             final Attr attr = (Attr) this;
556             final Attr otherAttr = (Attr) other;
557             if (!Objects.equals(attr.getName(), otherAttr.getName())
558                     || !Objects.equals(attr.getLocalName(), otherAttr.getLocalName())
559                     || !Objects.equals(attr.getValue(), otherAttr.getValue())) {
560                 return false;
561             }
562 
563         }
564         else if (this instanceof ProcessingInstruction) {
565             final ProcessingInstruction instruction = (ProcessingInstruction) this;
566             final ProcessingInstruction otherInstruction = (ProcessingInstruction) other;
567             if (!Objects.equals(instruction.getTarget(), otherInstruction.getTarget())
568                     || !Objects.equals(instruction.getData(), otherInstruction.getData())) {
569                 return false;
570             }
571 
572         }
573         else if (this instanceof Text || this instanceof Comment) {
574             final CharacterData data = (CharacterData) this;
575             final CharacterData otherData = (CharacterData) other;
576             if (!Objects.equals(data.getData(), otherData.getData())) {
577                 return false;
578             }
579         }
580 
581         final NodeList childNodes = getChildNodes();
582         final NodeList otherChildNodes = other.getChildNodes();
583         if (childNodes != null || otherChildNodes != null) {
584             if (childNodes == null || otherChildNodes == null) {
585                 return false;
586             }
587 
588             final int length = childNodes.getLength();
589             final int otherLength = childNodes.getLength();
590             if (length != otherLength) {
591                 return false;
592             }
593 
594             for (int i = 0; i < length; i++) {
595                 final Node childNode = (Node) childNodes.item(i);
596                 final Node otherChildNode = (Node) otherChildNodes.item(i);
597                 if (!childNode.isEqualNode(otherChildNode)) {
598                     return false;
599                 }
600             }
601         }
602 
603         return true;
604     }
605 
606     /**
607      * This method provides a way to determine whether two Node references returned by
608      * the implementation reference the same object.
609      * When two Node references are references to the same object, even if through a proxy,
610      * the references may be used completely interchangeably, such that all attributes
611      * have the same values and calling the same DOM method on either reference always has exactly the same effect.
612      *
613      * @param other the node to test against
614      *
615      * @return whether this node is the same node as the given one
616      */
617     @JsxFunction
618     public boolean isSameNode(final Object other) {
619         return other == this;
620     }
621 
622     /**
623      * Returns whether this node has any children.
624      * @return boolean true if this node has any children, false otherwise
625      */
626     @JsxFunction
627     public boolean hasChildNodes() {
628         return getDomNodeOrDie().getChildren().iterator().hasNext();
629     }
630 
631     /**
632      * @param namespace string containing the namespace to look the prefix up
633      * @return a string containing the prefix for a given namespace URI,
634      *         if present, and null if not. When multiple prefixes are possible,
635      *         the first prefix is returned.
636      */
637     @JsxFunction
638     public String lookupPrefix(final String namespace) {
639         return null;
640     }
641 
642     /**
643      * Returns the child nodes of the current element.
644      * @return the child nodes of the current element
645      */
646     @JsxGetter
647     public NodeList getChildNodes() {
648         if (childNodes_ == null) {
649             final DomNode node = getDomNodeOrDie();
650             childNodes_ = new NodeList(node, false);
651             childNodes_.setElementsSupplier(
652                     (Supplier<List<DomNode>> & Serializable)
653                     () -> {
654                         final List<DomNode> response = new ArrayList<>();
655                         for (final DomNode child : node.getChildren()) {
656                             response.add(child);
657                         }
658 
659                         return response;
660                     });
661         }
662         return childNodes_;
663     }
664 
665     /**
666      * Returns this node's parent node.
667      * @return this node's parent node
668      */
669     public final Node getParent() {
670         return getJavaScriptNode(getDomNodeOrDie().getParentNode());
671     }
672 
673     /**
674      * Gets the JavaScript property {@code parentNode} for the node that
675      * contains the current node.
676      * @return the parent node
677      */
678     @JsxGetter
679     public Object getParentNode() {
680         return getJavaScriptNode(getDomNodeOrDie().getParentNode());
681     }
682 
683     /**
684      * Gets the JavaScript property {@code nextSibling} for the node that
685      * contains the current node.
686      * @return the next sibling node or null if the current node has
687      *         no next sibling.
688      */
689     @JsxGetter
690     public Node getNextSibling() {
691         return getJavaScriptNode(getDomNodeOrDie().getNextSibling());
692     }
693 
694     /**
695      * Gets the JavaScript property {@code previousSibling} for the node that
696      * contains the current node.
697      * @return the previous sibling node or null if the current node has
698      *         no previous sibling.
699      */
700     @JsxGetter
701     public Node getPreviousSibling() {
702         return getJavaScriptNode(getDomNodeOrDie().getPreviousSibling());
703     }
704 
705     /**
706      * Gets the JavaScript property {@code firstChild} for the node that
707      * contains the current node.
708      * @return the first child node or null if the current node has
709      *         no children.
710      */
711     @JsxGetter
712     public Node getFirstChild() {
713         return getJavaScriptNode(getDomNodeOrDie().getFirstChild());
714     }
715 
716     /**
717      * Gets the JavaScript property {@code lastChild} for the node that
718      * contains the current node.
719      * @return the last child node or null if the current node has
720      *         no children.
721      */
722     @JsxGetter
723     public Node getLastChild() {
724         return getJavaScriptNode(getDomNodeOrDie().getLastChild());
725     }
726 
727     /**
728      * Gets the JavaScript node for a given DomNode.
729      * @param domNode the DomNode
730      * @return the JavaScript node or null if the DomNode was null
731      */
732     protected Node getJavaScriptNode(final DomNode domNode) {
733         if (domNode == null) {
734             return null;
735         }
736         return (Node) getScriptableFor(domNode);
737     }
738 
739     /**
740      * Returns the owner document.
741      * @return the document
742      */
743     @JsxGetter
744     public HtmlUnitScriptable getOwnerDocument() {
745         final Object document = getDomNodeOrDie().getOwnerDocument();
746         if (document != null) {
747             return ((SgmlPage) document).getScriptableObject();
748         }
749         return null;
750     }
751 
752     /**
753      * Returns the owner document.
754      * @return the document
755      */
756     @JsxFunction
757     public Node getRootNode() {
758         Node parent = this;
759         while (parent != null) {
760             if (parent instanceof Document || parent instanceof DocumentFragment) {
761                 return parent;
762             }
763             parent = parent.getParent();
764         }
765         return this;
766     }
767 
768     /**
769      * Compares the positions of this node and the provided node within the document.
770      * @param node node object that specifies the node to check
771      * @return how the node is positioned relatively to the reference node.
772      * @see <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition">DOM level 3</a>
773      * @see org.w3c.dom.Node#compareDocumentPosition(org.w3c.dom.Node)
774      */
775     @JsxFunction
776     public int compareDocumentPosition(final Object node) {
777         if (!(node instanceof Node)) {
778             throw JavaScriptEngine.typeError("Could not convert JavaScript argument arg 0");
779         }
780         return getDomNodeOrDie().compareDocumentPosition(((Node) node).getDomNodeOrDie());
781     }
782 
783     /**
784      * Merges adjacent TextNode objects to produce a normalized document object model.
785      */
786     @JsxFunction
787     public void normalize() {
788         getDomNodeOrDie().normalize();
789     }
790 
791     /**
792      * Gets the textContent attribute.
793      * @return the contents of this node as text
794      */
795     @JsxGetter
796     public String getTextContent() {
797         return getDomNodeOrDie().getTextContent();
798     }
799 
800     /**
801      * Replace all children elements of this element with the supplied value.
802      * @param value - the new value for the contents of this node
803      */
804     @JsxSetter
805     public void setTextContent(final Object value) {
806         getDomNodeOrDie().setTextContent(value == null ? null : JavaScriptEngine.toString(value));
807     }
808 
809     /**
810      * Gets the JavaScript property {@code parentElement}.
811      * @return the parent element
812      * @see #getParentNode()
813      */
814     @JsxGetter
815     public Element getParentElement() {
816         final Node parent = getParent();
817         if (!(parent instanceof Element)) {
818             return null;
819         }
820         return (Element) parent;
821     }
822 
823     /**
824      * Returns the attributes of this XML element.
825      * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/Node.attributes">Gecko DOM Reference</a>
826      * @return the attributes of this XML element
827      */
828     public NamedNodeMap getAttributes() {
829         return null;
830     }
831 
832     /**
833      * Checks whether the given element is contained within this object.
834      * @param element element object that specifies the element to check
835      * @return true if the element is contained within this object
836      */
837     @JsxFunction
838     public boolean contains(final Object element) {
839         if (element == null || JavaScriptEngine.isUndefined(element)) {
840             return false;
841         }
842 
843         if (!(element instanceof Node)) {
844             throw JavaScriptEngine.reportRuntimeError("Could not convert JavaScript argument arg 0");
845         }
846 
847         for (Node parent = (Node) element; parent != null; parent = parent.getParentElement()) {
848             if (this == parent) {
849                 return true;
850             }
851         }
852         return false;
853     }
854 
855     /**
856      * Returns the Base URI as a string.
857      * @return the Base URI as a string
858      */
859     @JsxGetter
860     public String getBaseURI() {
861         return getDomNodeOrDie().getBaseURI();
862     }
863 
864     /**
865      * Returns true when the current element has any attributes or not.
866      * @return true if an attribute is specified on this element
867      */
868     public boolean hasAttributes() {
869         return getDomNodeOrDie().hasAttributes();
870     }
871 
872     /**
873      * Returns the namespace prefix.
874      * @return the namespace prefix
875      */
876     public String getPrefix() {
877         return getDomNodeOrDie().getPrefix();
878     }
879 
880     /**
881      * Returns the local name of this attribute.
882      * @return the local name of this attribute
883      */
884     public String getLocalName() {
885         return getDomNodeOrDie().getLocalName();
886     }
887 
888     /**
889      * Returns the URI that identifies an XML namespace.
890      * @return the URI that identifies an XML namespace
891      */
892     public String getNamespaceURI() {
893         return getDomNodeOrDie().getNamespaceURI();
894     }
895 
896     /**
897      * Returns the current number of child elements.
898      * @return the child element count
899      */
900     protected int getChildElementCount() {
901         final DomNode domNode = getDomNodeOrDie();
902         if (domNode instanceof DomElement) {
903             return ((DomElement) domNode).getChildElementCount();
904         }
905 
906         int counter = 0;
907         for (final DomNode child : getDomNodeOrDie().getChildren()) {
908             if (child != null) {
909                 final HtmlUnitScriptable scriptable = child.getScriptableObject();
910                 if (scriptable instanceof Element) {
911                     counter++;
912                 }
913             }
914         }
915         return counter;
916     }
917 
918     /**
919      * Returns the first element child.
920      * @return the first element child
921      */
922     protected Element getFirstElementChild() {
923         final DomNode domNode = getDomNodeOrDie();
924         if (domNode instanceof DomElement) {
925             final DomElement child = ((DomElement) domNode).getFirstElementChild();
926             if (child != null) {
927                 return child.getScriptableObject();
928             }
929             return null;
930         }
931 
932         for (final DomNode child : domNode.getChildren()) {
933             if (child != null) {
934                 final HtmlUnitScriptable scriptable = child.getScriptableObject();
935                 if (scriptable instanceof Element) {
936                     return (Element) scriptable;
937                 }
938             }
939         }
940         return null;
941     }
942 
943     /**
944      * Returns the last element child.
945      * @return the last element child
946      */
947     protected Element getLastElementChild() {
948         final DomNode domNode = getDomNodeOrDie();
949         if (domNode instanceof DomElement) {
950             final DomElement child = ((DomElement) getDomNodeOrDie()).getLastElementChild();
951             if (child != null) {
952                 return child.getScriptableObject();
953             }
954             return null;
955         }
956 
957         Element result = null;
958         for (final DomNode child : domNode.getChildren()) {
959             final HtmlUnitScriptable scriptable = child.getScriptableObject();
960             if (scriptable instanceof Element) {
961                 result = (Element) scriptable;
962             }
963         }
964         return result;
965     }
966 
967     /**
968      * Gets the children of the current node.
969      * @see <a href="http://msdn.microsoft.com/en-us/library/ms537446.aspx">MSDN documentation</a>
970      * @return the child at the given position
971      */
972     protected HTMLCollection getChildren() {
973         final DomNode node = getDomNodeOrDie();
974         final HTMLCollection childrenColl = new HTMLCollection(node, false);
975         childrenColl.setElementsSupplier(
976                 (Supplier<List<DomNode>> & Serializable)
977                 () -> {
978                     final List<DomNode> children = new ArrayList<>();
979                     for (final DomNode domNode : node.getChildNodes()) {
980                         if (domNode instanceof DomElement) {
981                             children.add(domNode);
982                         }
983                     }
984                     return children;
985                 });
986         return childrenColl;
987     }
988 
989     /**
990      * Inserts a set of Node or DOMString objects in the children list of this ChildNode's parent,
991      * just after this ChildNode.
992      * @param context the context
993      * @param thisObj this object
994      * @param args the arguments
995      * @param function the function
996      */
997     protected static void after(final Context context, final Scriptable thisObj, final Object[] args,
998             final Function function) {
999         final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
1000         final DomNode parentNode = thisDomNode.getParentNode();
1001         final DomNode nextSibling = thisDomNode.getNextSibling();
1002         for (final Object arg : args) {
1003             final Node node = toNodeOrTextNode((Node) thisObj, arg);
1004             final DomNode newNode = node.getDomNodeOrDie();
1005             if (nextSibling == null) {
1006                 parentNode.appendChild(newNode);
1007             }
1008             else {
1009                 nextSibling.insertBefore(newNode);
1010             }
1011         }
1012     }
1013 
1014     /**
1015      * Inserts a set of Node objects or string objects after the last child of the Element.
1016      * String objects are inserted as equivalent Text nodes.
1017      * @param context the context
1018      * @param thisObj this object
1019      * @param args the arguments
1020      * @param function the function
1021      */
1022     protected static void append(final Context context, final Scriptable thisObj, final Object[] args,
1023             final Function function) {
1024         if (!(thisObj instanceof Node)) {
1025             throw JavaScriptEngine.typeError("Illegal invocation");
1026         }
1027 
1028         final Node thisNode = (Node) thisObj;
1029         final DomNode thisDomNode = thisNode.getDomNodeOrDie();
1030 
1031         for (final Object arg : args) {
1032             final Node node = toNodeOrTextNode(thisNode, arg);
1033             thisDomNode.appendChild(node.getDomNodeOrDie());
1034         }
1035     }
1036 
1037     /**
1038      * Inserts a set of Node objects or string objects before the first child of the Element.
1039      * String objects are inserted as equivalent Text nodes.
1040      * @param context the context
1041      * @param thisObj this object
1042      * @param args the arguments
1043      * @param function the function
1044      */
1045     protected static void prepend(final Context context, final Scriptable thisObj, final Object[] args,
1046             final Function function) {
1047         if (!(thisObj instanceof Node)) {
1048             throw JavaScriptEngine.typeError("Illegal invocation");
1049         }
1050 
1051         final Node thisNode = (Node) thisObj;
1052         final DomNode thisDomNode = thisNode.getDomNodeOrDie();
1053         final DomNode firstChild = thisDomNode.getFirstChild();
1054 
1055         for (final Object arg : args) {
1056             final Node node = toNodeOrTextNode(thisNode, arg);
1057             final DomNode newNode = node.getDomNodeOrDie();
1058             if (firstChild == null) {
1059                 thisDomNode.appendChild(newNode);
1060             }
1061             else {
1062                 firstChild.insertBefore(newNode);
1063             }
1064         }
1065     }
1066 
1067     /**
1068      * Replaces the existing children of a Node with a specified new set of children.
1069      * These can be string or Node objects.
1070      * @param context the context
1071      * @param thisObj this object
1072      * @param args the arguments
1073      * @param function the function
1074      */
1075     protected static void replaceChildren(final Context context, final Scriptable thisObj, final Object[] args,
1076             final Function function) {
1077         if (!(thisObj instanceof Node)) {
1078             throw JavaScriptEngine.typeError("Illegal invocation");
1079         }
1080 
1081         final Node thisNode = (Node) thisObj;
1082         final DomNode thisDomNode = thisNode.getDomNodeOrDie();
1083         thisDomNode.removeAllChildren();
1084 
1085         for (final Object arg : args) {
1086             final Node node = toNodeOrTextNode(thisNode, arg);
1087             thisDomNode.appendChild(node.getDomNodeOrDie());
1088         }
1089     }
1090 
1091     private static Node toNodeOrTextNode(final Node thisObj, final Object obj) {
1092         if (obj instanceof Node) {
1093             return (Node) obj;
1094         }
1095         return (Node)
1096                 ((HTMLDocument) thisObj.getOwnerDocument()).createTextNode(JavaScriptEngine.toString(obj));
1097     }
1098 
1099     /**
1100      * Inserts a set of Node or DOMString objects in the children list of this ChildNode's parent,
1101      * just before this ChildNode.
1102      * @param context the context
1103      * @param thisObj this object
1104      * @param args the arguments
1105      * @param function the function
1106      */
1107     protected static void before(final Context context, final Scriptable thisObj, final Object[] args,
1108             final Function function) {
1109         for (final Object arg : args) {
1110             final Node node = toNodeOrTextNode((Node) thisObj, arg);
1111             ((Node) thisObj).getDomNodeOrDie().insertBefore(node.getDomNodeOrDie());
1112         }
1113     }
1114 
1115     /**
1116      * Replaces this ChildNode in the children list of its parent with a set of Node or DOMString objects.
1117      * @param context the context
1118      * @param thisObj this object
1119      * @param args the arguments
1120      * @param function the function
1121      */
1122     protected static void replaceWith(final Context context, final Scriptable thisObj, final Object[] args,
1123             final Function function) {
1124         final DomNode thisDomNode = ((Node) thisObj).getDomNodeOrDie();
1125         final DomNode parentNode = thisDomNode.getParentNode();
1126 
1127         if (args.length == 0) {
1128             parentNode.removeChild(thisDomNode);
1129             return;
1130         }
1131 
1132         final DomNode nextSibling = thisDomNode.getNextSibling();
1133         boolean isFirst = true;
1134         for (final Object arg : args) {
1135             final DomNode newNode = toNodeOrTextNode((Node) thisObj, arg).getDomNodeOrDie();
1136             if (isFirst) {
1137                 isFirst = false;
1138                 thisDomNode.replace(newNode);
1139             }
1140             else {
1141                 if (nextSibling == null) {
1142                     parentNode.appendChild(newNode);
1143                 }
1144                 else {
1145                     nextSibling.insertBefore(newNode);
1146                 }
1147             }
1148         }
1149     }
1150 }