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