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