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