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.html;
16  
17  import java.io.IOException;
18  import java.io.PrintWriter;
19  import java.io.Serializable;
20  import java.io.StringWriter;
21  import java.nio.charset.Charset;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.NoSuchElementException;
28  
29  import org.htmlunit.BrowserVersionFeatures;
30  import org.htmlunit.IncorrectnessListener;
31  import org.htmlunit.Page;
32  import org.htmlunit.SgmlPage;
33  import org.htmlunit.WebAssert;
34  import org.htmlunit.WebClient;
35  import org.htmlunit.WebClient.PooledCSS3Parser;
36  import org.htmlunit.WebWindow;
37  import org.htmlunit.css.ComputedCssStyleDeclaration;
38  import org.htmlunit.css.CssStyleSheet;
39  import org.htmlunit.css.StyleAttributes;
40  import org.htmlunit.cssparser.parser.CSSErrorHandler;
41  import org.htmlunit.cssparser.parser.CSSException;
42  import org.htmlunit.cssparser.parser.CSSOMParser;
43  import org.htmlunit.cssparser.parser.CSSParseException;
44  import org.htmlunit.cssparser.parser.selector.Selector;
45  import org.htmlunit.cssparser.parser.selector.SelectorList;
46  import org.htmlunit.html.HtmlElement.DisplayStyle;
47  import org.htmlunit.html.serializer.HtmlSerializerNormalizedText;
48  import org.htmlunit.html.serializer.HtmlSerializerVisibleText;
49  import org.htmlunit.html.xpath.XPathHelper;
50  import org.htmlunit.javascript.HtmlUnitScriptable;
51  import org.htmlunit.javascript.host.event.Event;
52  import org.htmlunit.xpath.xml.utils.PrefixResolver;
53  import org.w3c.dom.DOMException;
54  import org.w3c.dom.Document;
55  import org.w3c.dom.NamedNodeMap;
56  import org.w3c.dom.Node;
57  import org.w3c.dom.UserDataHandler;
58  import org.xml.sax.SAXException;
59  
60  /**
61   * Base class for nodes in the HTML DOM tree. This class is modeled after the
62   * W3C DOM specification, but does not implement it.
63   *
64   * @author Mike Bowler
65   * @author Mike J. Bresnahan
66   * @author David K. Taylor
67   * @author Christian Sell
68   * @author Chris Erskine
69   * @author Mike Williams
70   * @author Marc Guillemot
71   * @author Denis N. Antonioli
72   * @author Daniel Gredler
73   * @author Ahmed Ashour
74   * @author Rodney Gitzel
75   * @author Sudhan Moghe
76   * @author Tom Anderson
77   * @author Ronald Brill
78   * @author Chuck Dumont
79   * @author Frank Danek
80   * @author Lai Quang Duong
81   */
82  public abstract class DomNode implements Cloneable, Serializable, Node {
83  
84      /** A ready state constant (state 1). */
85      public static final String READY_STATE_UNINITIALIZED = "uninitialized";
86  
87      /** A ready state constant (state 2). */
88      public static final String READY_STATE_LOADING = "loading";
89  
90      /** A ready state constant (state 3). */
91      public static final String READY_STATE_LOADED = "loaded";
92  
93      /** A ready state constant (state 4). */
94      public static final String READY_STATE_INTERACTIVE = "interactive";
95  
96      /** A ready state constant (state 5). */
97      public static final String READY_STATE_COMPLETE = "complete";
98  
99      /** The name of the "element" property. Used when watching property change events. */
100     public static final String PROPERTY_ELEMENT = "element";
101 
102     private static final NamedNodeMap EMPTY_NAMED_NODE_MAP = new ReadOnlyEmptyNamedNodeMapImpl();
103 
104     /** The owning page of this node. */
105     private SgmlPage page_;
106 
107     /** The parent node. */
108     private DomNode parent_;
109 
110     /**
111      * The previous sibling. The first child's <code>previousSibling</code> points
112      * to the end of the list
113      */
114     private DomNode previousSibling_;
115 
116     /**
117      * The next sibling. The last child's <code>nextSibling</code> is {@code null}
118      */
119     private DomNode nextSibling_;
120 
121     /** Start of the child list. */
122     private DomNode firstChild_;
123 
124     /**
125      * This is the JavaScript object corresponding to this DOM node. It may
126      * be null if there isn't a corresponding JavaScript object.
127      */
128     private HtmlUnitScriptable scriptObject_;
129 
130     /** The ready state is a value that is available to a large number of elements. */
131     private String readyState_;
132 
133     /**
134      * The line number in the source page where the DOM node starts.
135      */
136     private int startLineNumber_ = -1;
137 
138     /**
139      * The column number in the source page where the DOM node starts.
140      */
141     private int startColumnNumber_ = -1;
142 
143     /**
144      * The line number in the source page where the DOM node ends.
145      */
146     private int endLineNumber_ = -1;
147 
148     /**
149      * The column number in the source page where the DOM node ends.
150      */
151     private int endColumnNumber_ = -1;
152 
153     private boolean attachedToPage_;
154 
155     /** The listeners which are to be notified of characterData change. */
156     private List<CharacterDataChangeListener> characterDataListeners_;
157     private List<DomChangeListener> domListeners_;
158 
159     private Map<String, Object> userData_;
160 
161     /**
162      * Creates a new instance.
163      * @param page the page which contains this node
164      */
165     protected DomNode(final SgmlPage page) {
166         readyState_ = READY_STATE_LOADING;
167         page_ = page;
168     }
169 
170     /**
171      * Sets the line and column numbers in the source page where the DOM node starts.
172      *
173      * @param startLineNumber the line number where the DOM node starts
174      * @param startColumnNumber the column number where the DOM node starts
175      */
176     public void setStartLocation(final int startLineNumber, final int startColumnNumber) {
177         startLineNumber_ = startLineNumber;
178         startColumnNumber_ = startColumnNumber;
179     }
180 
181     /**
182      * Sets the line and column numbers in the source page where the DOM node ends.
183      *
184      * @param endLineNumber the line number where the DOM node ends
185      * @param endColumnNumber the column number where the DOM node ends
186      */
187     public void setEndLocation(final int endLineNumber, final int endColumnNumber) {
188         endLineNumber_ = endLineNumber;
189         endColumnNumber_ = endColumnNumber;
190     }
191 
192     /**
193      * Returns the line number in the source page where the DOM node starts.
194      * @return the line number in the source page where the DOM node starts
195      */
196     public int getStartLineNumber() {
197         return startLineNumber_;
198     }
199 
200     /**
201      * Returns the column number in the source page where the DOM node starts.
202      * @return the column number in the source page where the DOM node starts
203      */
204     public int getStartColumnNumber() {
205         return startColumnNumber_;
206     }
207 
208     /**
209      * Returns the line number in the source page where the DOM node ends.
210      * @return 0 if no information on the line number is available (for instance for nodes dynamically added),
211      *         -1 if the end tag has not yet been parsed (during page loading)
212      */
213     public int getEndLineNumber() {
214         return endLineNumber_;
215     }
216 
217     /**
218      * Returns the column number in the source page where the DOM node ends.
219      * @return 0 if no information on the line number is available (for instance for nodes dynamically added),
220      *         -1 if the end tag has not yet been parsed (during page loading)
221      */
222     public int getEndColumnNumber() {
223         return endColumnNumber_;
224     }
225 
226     /**
227      * Returns the page that contains this node.
228      * @return the page that contains this node
229      */
230     public SgmlPage getPage() {
231         return page_;
232     }
233 
234     /**
235      * Returns the page that contains this node.
236      * @return the page that contains this node
237      */
238     public HtmlPage getHtmlPageOrNull() {
239         if (page_ == null || !page_.isHtmlPage()) {
240             return null;
241         }
242         return (HtmlPage) page_;
243     }
244 
245     /**
246      * {@inheritDoc}
247      */
248     @Override
249     public Document getOwnerDocument() {
250         return getPage();
251     }
252 
253     /**
254      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
255      *
256      * Sets the JavaScript object that corresponds to this node. This is not guaranteed to be set even if
257      * there is a JavaScript object for this DOM node.
258      *
259      * @param scriptObject the JavaScript object
260      */
261     public void setScriptableObject(final HtmlUnitScriptable scriptObject) {
262         scriptObject_ = scriptObject;
263     }
264 
265     /**
266      * {@inheritDoc}
267      */
268     @Override
269     public DomNode getLastChild() {
270         if (firstChild_ != null) {
271             // last child is stored as the previous sibling of first child
272             return firstChild_.previousSibling_;
273         }
274         return null;
275     }
276 
277     /**
278      * {@inheritDoc}
279      */
280     @Override
281     public DomNode getParentNode() {
282         return parent_;
283     }
284 
285     /**
286      * Sets the parent node.
287      * @param parent the parent node
288      */
289     protected void setParentNode(final DomNode parent) {
290         parent_ = parent;
291     }
292 
293     /**
294      * Returns this node's index within its parent's child nodes (zero-based).
295      * @return this node's index within its parent's child nodes (zero-based)
296      */
297     public int getIndex() {
298         int index = 0;
299         for (DomNode n = previousSibling_; n != null && n.nextSibling_ != null; n = n.previousSibling_) {
300             index++;
301         }
302         return index;
303     }
304 
305     /**
306      * {@inheritDoc}
307      */
308     @Override
309     public DomNode getPreviousSibling() {
310         if (parent_ == null || this == parent_.firstChild_) {
311             // previous sibling of first child points to last child
312             return null;
313         }
314         return previousSibling_;
315     }
316 
317     /**
318      * {@inheritDoc}
319      */
320     @Override
321     public DomNode getNextSibling() {
322         return nextSibling_;
323     }
324 
325     /**
326      * {@inheritDoc}
327      */
328     @Override
329     public DomNode getFirstChild() {
330         return firstChild_;
331     }
332 
333     /**
334      * Returns {@code true} if this node is an ancestor of the specified node.
335      *
336      * @param node the node to check
337      * @return {@code true} if this node is an ancestor of the specified node
338      */
339     public boolean isAncestorOf(final DomNode node) {
340         DomNode parent = node;
341         while (parent != null) {
342             if (parent == this) {
343                 return true;
344             }
345             parent = parent.getParentNode();
346         }
347         return false;
348     }
349 
350     /**
351      * Returns {@code true} if this node is an ancestor of the specified nodes.
352      *
353      * @param nodes the nodes to check
354      * @return {@code true} if this node is an ancestor of the specified nodes
355      */
356     public boolean isAncestorOfAny(final DomNode... nodes) {
357         for (final DomNode node : nodes) {
358             if (isAncestorOf(node)) {
359                 return true;
360             }
361         }
362         return false;
363     }
364 
365     /**
366      * {@inheritDoc}
367      */
368     @Override
369     public String getNamespaceURI() {
370         return null;
371     }
372 
373     /**
374      * {@inheritDoc}
375      */
376     @Override
377     public String getLocalName() {
378         return null;
379     }
380 
381     /**
382      * {@inheritDoc}
383      */
384     @Override
385     public String getPrefix() {
386         return null;
387     }
388 
389     /**
390      * {@inheritDoc}
391      */
392     @Override
393     public boolean hasChildNodes() {
394         return firstChild_ != null;
395     }
396 
397     /**
398      * {@inheritDoc}
399      */
400     @Override
401     public DomNodeList<DomNode> getChildNodes() {
402         return new SiblingDomNodeList(this);
403     }
404 
405     /**
406      * {@inheritDoc}
407      * Not yet implemented.
408      */
409     @Override
410     public boolean isSupported(final String namespace, final String featureName) {
411         throw new UnsupportedOperationException("DomNode.isSupported is not yet implemented.");
412     }
413 
414     /**
415      * {@inheritDoc}
416      */
417     @Override
418     public void normalize() {
419         for (DomNode child = getFirstChild(); child != null; child = child.getNextSibling()) {
420             if (child instanceof DomText) {
421                 final StringBuilder dataBuilder = new StringBuilder();
422                 DomNode toRemove = child;
423                 DomText firstText = null;
424                 //IE removes all child text nodes, but FF preserves the first
425                 while (toRemove instanceof DomText && !(toRemove instanceof DomCDataSection)) {
426                     final DomNode nextChild = toRemove.getNextSibling();
427                     dataBuilder.append(toRemove.getTextContent());
428                     if (firstText != null) {
429                         toRemove.remove();
430                     }
431                     if (firstText == null) {
432                         firstText = (DomText) toRemove;
433                     }
434                     toRemove = nextChild;
435                 }
436                 if (firstText != null) {
437                     firstText.setData(dataBuilder.toString());
438                 }
439             }
440         }
441     }
442 
443     /**
444      * {@inheritDoc}
445      */
446     @Override
447     public String getBaseURI() {
448         return getPage().getUrl().toExternalForm();
449     }
450 
451     /**
452      * {@inheritDoc}
453      */
454     @Override
455     public short compareDocumentPosition(final Node other) {
456         if (other == this) {
457             return 0; // strange, no constant available?
458         }
459 
460         // get ancestors of both
461         final List<Node> myAncestors = getAncestors();
462         final List<Node> otherAncestors = ((DomNode) other).getAncestors();
463 
464         if (!myAncestors.get(0).equals(otherAncestors.get(0))) {
465             // spec likes to have a consistent order
466             //
467             // If ... node1’s root is not node2’s root, then return the result of adding
468             // DOCUMENT_POSITION_DISCONNECTED, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,
469             // and either DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING,
470             // with the constraint that this is to be consistent...
471             if (this.hashCode() < other.hashCode()) {
472                 return DOCUMENT_POSITION_DISCONNECTED
473                         | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
474                         | DOCUMENT_POSITION_PRECEDING;
475             }
476 
477             return DOCUMENT_POSITION_DISCONNECTED
478                     | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
479                     | DOCUMENT_POSITION_FOLLOWING;
480         }
481 
482         final int max = Math.min(myAncestors.size(), otherAncestors.size());
483 
484         int i = 1;
485         while (i < max && myAncestors.get(i) == otherAncestors.get(i)) {
486             i++;
487         }
488 
489         if (i != 1 && i == max) {
490             if (myAncestors.size() == max) {
491                 return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING;
492             }
493             return DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING;
494         }
495 
496         if (max == 1) {
497             if (myAncestors.contains(other)) {
498                 return DOCUMENT_POSITION_CONTAINS;
499             }
500             if (otherAncestors.contains(this)) {
501                 return DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING;
502             }
503             return DOCUMENT_POSITION_DISCONNECTED | DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
504         }
505 
506         // neither contains nor contained by
507         final Node myAncestor = myAncestors.get(i);
508         final Node otherAncestor = otherAncestors.get(i);
509         Node node = myAncestor;
510         while (node != otherAncestor && node != null) {
511             node = node.getPreviousSibling();
512         }
513         if (node == null) {
514             return DOCUMENT_POSITION_FOLLOWING;
515         }
516         return DOCUMENT_POSITION_PRECEDING;
517     }
518 
519     /**
520      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
521      *
522      * Gets the ancestors of the node.
523      * @return a list of the ancestors with the root at the first position
524      */
525     public List<Node> getAncestors() {
526         final List<Node> list = new ArrayList<>();
527         list.add(this);
528 
529         Node node = getParentNode();
530         while (node != null) {
531             list.add(0, node);
532             node = node.getParentNode();
533         }
534         return list;
535     }
536 
537     /**
538      * {@inheritDoc}
539      */
540     @Override
541     public String getTextContent() {
542         switch (getNodeType()) {
543             case ELEMENT_NODE:
544             case ATTRIBUTE_NODE:
545             case ENTITY_NODE:
546             case ENTITY_REFERENCE_NODE:
547             case DOCUMENT_FRAGMENT_NODE:
548                 final StringBuilder builder = new StringBuilder();
549                 for (final DomNode child : getChildren()) {
550                     final short childType = child.getNodeType();
551                     if (childType != COMMENT_NODE && childType != PROCESSING_INSTRUCTION_NODE) {
552                         builder.append(child.getTextContent());
553                     }
554                 }
555                 return builder.toString();
556 
557             case TEXT_NODE:
558             case CDATA_SECTION_NODE:
559             case COMMENT_NODE:
560             case PROCESSING_INSTRUCTION_NODE:
561                 return getNodeValue();
562 
563             default:
564                 return null;
565         }
566     }
567 
568     /**
569      * {@inheritDoc}
570      */
571     @Override
572     public void setTextContent(final String textContent) {
573         removeAllChildren();
574         if (textContent != null && !textContent.isEmpty()) {
575             appendChild(new DomText(getPage(), textContent));
576         }
577     }
578 
579     /**
580      * {@inheritDoc}
581      */
582     @Override
583     public boolean isSameNode(final Node other) {
584         return other == this;
585     }
586 
587     /**
588      * {@inheritDoc}
589      * Not yet implemented.
590      */
591     @Override
592     public String lookupPrefix(final String namespaceURI) {
593         throw new UnsupportedOperationException("DomNode.lookupPrefix is not yet implemented.");
594     }
595 
596     /**
597      * {@inheritDoc}
598      * Not yet implemented.
599      */
600     @Override
601     public boolean isDefaultNamespace(final String namespaceURI) {
602         throw new UnsupportedOperationException("DomNode.isDefaultNamespace is not yet implemented.");
603     }
604 
605     /**
606      * {@inheritDoc}
607      * Not yet implemented.
608      */
609     @Override
610     public String lookupNamespaceURI(final String prefix) {
611         throw new UnsupportedOperationException("DomNode.lookupNamespaceURI is not yet implemented.");
612     }
613 
614     /**
615      * {@inheritDoc}
616      * Not yet implemented.
617      */
618     @Override
619     public boolean isEqualNode(final Node arg) {
620         throw new UnsupportedOperationException("DomNode.isEqualNode is not yet implemented.");
621     }
622 
623     /**
624      * {@inheritDoc}
625      * Not yet implemented.
626      */
627     @Override
628     public Object getFeature(final String feature, final String version) {
629         throw new UnsupportedOperationException("DomNode.getFeature is not yet implemented.");
630     }
631 
632     /**
633      * {@inheritDoc}
634      */
635     @Override
636     public Object getUserData(final String key) {
637         Object value = null;
638         if (userData_ != null) {
639             value = userData_.get(key);
640         }
641         return value;
642     }
643 
644     /**
645      * {@inheritDoc}
646      */
647     @Override
648     public Object setUserData(final String key, final Object data, final UserDataHandler handler) {
649         if (userData_ == null) {
650             userData_ = new HashMap<>();
651         }
652         return userData_.put(key, data);
653     }
654 
655     /**
656      * {@inheritDoc}
657      */
658     @Override
659     public boolean hasAttributes() {
660         return false;
661     }
662 
663     /**
664      * {@inheritDoc}
665      */
666     @Override
667     public NamedNodeMap getAttributes() {
668         return EMPTY_NAMED_NODE_MAP;
669     }
670 
671     /**
672      * <p>Returns {@code true} if this node is displayed and can be visible to the user
673      * (ignoring screen size, scrolling limitations, color, font-size, or overlapping nodes).</p>
674      *
675      * <p><b>NOTE:</b> If CSS is
676      * {@link org.htmlunit.WebClientOptions#setCssEnabled(boolean) disabled}, this method
677      * does <b>not</b> take this element's style into consideration!</p>
678      *
679      * @see <a href="http://www.w3.org/TR/CSS2/visufx.html#visibility">CSS2 Visibility</a>
680      * @see <a href="http://www.w3.org/TR/CSS2/visuren.html#propdef-display">CSS2 Display</a>
681      * @see <a href="http://msdn.microsoft.com/en-us/library/ms531180.aspx">MSDN Documentation</a>
682      * @return {@code true} if the node is visible to the user, {@code false} otherwise
683      * @see #mayBeDisplayed()
684      */
685     public boolean isDisplayed() {
686         if (!mayBeDisplayed()) {
687             return false;
688         }
689 
690         final Page page = getPage();
691         final WebWindow window = page.getEnclosingWindow();
692         final WebClient webClient = window.getWebClient();
693         if (webClient.getOptions().isCssEnabled()) {
694             // display: iterate top to bottom, because if a parent is display:none,
695             // there's nothing that a child can do to override it
696             final List<Node> ancestors = getAncestors();
697             final ArrayList<ComputedCssStyleDeclaration> styles = new ArrayList<>(ancestors.size());
698 
699             for (final Node node : ancestors) {
700                 if (node instanceof HtmlElement elem) {
701                     if (elem.isHidden()) {
702                         return false;
703                     }
704 
705                     if (elem instanceof HtmlDialog dialog) {
706                         if (!dialog.isOpen()) {
707                             return false;
708                         }
709                     }
710                     else {
711                         final ComputedCssStyleDeclaration style = window.getComputedStyle(elem, null);
712                         if (DisplayStyle.NONE.value().equals(style.getDisplay())) {
713                             return false;
714                         }
715                         styles.add(style);
716                     }
717                 }
718             }
719 
720             // visibility: iterate bottom to top, because children can override
721             // the visibility used by parent nodes
722             for (int i = styles.size() - 1; i >= 0; i--) {
723                 final ComputedCssStyleDeclaration style = styles.get(i);
724                 final String visibility = style.getStyleAttribute(StyleAttributes.Definition.VISIBILITY, true);
725                 if (visibility.length() > 5) {
726                     if ("visible".equals(visibility)) {
727                         return true;
728                     }
729                     if ("hidden".equals(visibility) || "collapse".equals(visibility)) {
730                         return false;
731                     }
732                 }
733             }
734         }
735         return true;
736     }
737 
738     /**
739      * Returns {@code true} if nodes of this type can ever be displayed, {@code false} otherwise. Examples of nodes
740      * that can never be displayed are <code>&lt;head&gt;</code>,
741      * <code>&lt;meta&gt;</code>, <code>&lt;script&gt;</code>, etc.
742      * @return {@code true} if nodes of this type can ever be displayed, {@code false} otherwise
743      * @see #isDisplayed()
744      */
745     public boolean mayBeDisplayed() {
746         return true;
747     }
748 
749     /**
750      * Returns a normalized textual representation of this element that represents
751      * what would be visible to the user if this page was shown in a web browser.
752      * Whitespace is normalized like in the browser and block tags are separated by '\n'.
753      *
754      * @return a normalized textual representation of this element
755      */
756     public String asNormalizedText() {
757         final HtmlSerializerNormalizedText ser = new HtmlSerializerNormalizedText();
758         return ser.asText(this);
759     }
760 
761     /**
762      * Returns a textual representation of this element in the same way as
763      * the selenium/WebDriver WebElement#getText() property does.<br>
764      * see <a href="https://w3c.github.io/webdriver/#get-element-text">get-element-text</a> and
765      * <a href="https://w3c.github.io/webdriver/#dfn-bot-dom-getvisibletext">dfn-bot-dom-getvisibletext</a>
766      * Note: this is different from {@link #asNormalizedText()}
767      *
768      * @return a textual representation of this element that represents what would
769      *         be visible to the user if this page was shown in a web browser
770      */
771     public String getVisibleText() {
772         final HtmlSerializerVisibleText ser = new HtmlSerializerVisibleText();
773         return ser.asText(this);
774     }
775 
776     /**
777      * Returns a string representation as XML document from this element and all it's children (recursively).<br>
778      * The charset used in the xml header is the current page encoding; but the result is still a string.
779      * You have to make sure to use the correct (in fact the same) encoding if you write this to a file.<br>
780      * This serializes the current state of the DomTree - this implies that the content of noscript tags
781      * usually serialized as string because the content is converted during parsing (if js was enabled at that time).
782      * @return the XML string
783      */
784     public String asXml() {
785         Charset charsetName = null;
786         final HtmlPage htmlPage = getHtmlPageOrNull();
787         if (htmlPage != null) {
788             charsetName = htmlPage.getCharset();
789         }
790 
791         final StringWriter stringWriter = new StringWriter();
792         try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
793             boolean tag = false;
794             if (charsetName != null && this instanceof HtmlHtml) {
795                 printWriter.print("<?xml version=\"1.0\" encoding=\"");
796                 printWriter.print(charsetName);
797                 printWriter.print("\"?>");
798                 tag = true;
799             }
800             printXml("", tag, printWriter);
801             return stringWriter.toString();
802         }
803     }
804 
805     /**
806      * Recursively writes the XML data for the node tree starting at <code>node</code>.
807      *
808      * @param indent white space to indent child nodes
809      * @param tagBefore true if the last thing printed was a tag
810      * @param printWriter writer where child nodes are written
811      * @return true if the last thing printed was a tag
812      */
813     protected boolean printXml(final String indent, final boolean tagBefore, final PrintWriter printWriter) {
814         if (tagBefore) {
815             printWriter.print("\r\n");
816             printWriter.print(indent);
817         }
818         printWriter.print(this);
819         return printChildrenAsXml(indent, false, printWriter);
820     }
821 
822     /**
823      * Recursively writes the XML data for the node tree starting at <code>node</code>.
824      *
825      * @param indent white space to indent child nodes
826      * @param tagBefore true if the last thing printed was a tag
827      * @param printWriter writer where child nodes are written
828      * @return true if the last thing printed was a tag
829      */
830     protected boolean printChildrenAsXml(final String indent, final boolean tagBefore, final PrintWriter printWriter) {
831         DomNode child = getFirstChild();
832         boolean tag = tagBefore;
833         while (child != null) {
834             tag = child.printXml(indent + "  ", tag, printWriter);
835             child = child.getNextSibling();
836         }
837         return tag;
838     }
839 
840     /**
841      * {@inheritDoc}
842      */
843     @Override
844     public String getNodeValue() {
845         return null;
846     }
847 
848     /**
849      * {@inheritDoc}
850      */
851     @Override
852     public DomNode cloneNode(final boolean deep) {
853         final DomNode newnode;
854         try {
855             newnode = (DomNode) clone();
856         }
857         catch (final CloneNotSupportedException e) {
858             throw new IllegalStateException("Clone not supported for node [" + this + "]", e);
859         }
860 
861         newnode.parent_ = null;
862         newnode.nextSibling_ = null;
863         newnode.previousSibling_ = null;
864         newnode.scriptObject_ = null;
865         newnode.firstChild_ = null;
866         newnode.attachedToPage_ = false;
867 
868         // if deep, clone the children too.
869         if (deep) {
870             for (DomNode child = firstChild_; child != null; child = child.nextSibling_) {
871                 newnode.appendChild(child.cloneNode(true));
872             }
873         }
874 
875         return newnode;
876     }
877 
878     /**
879      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
880      *
881      * <p>Returns the JavaScript object that corresponds to this node, lazily initializing a new one if necessary.</p>
882      *
883      * <p>The logic of when and where the JavaScript object is created needs a cleanup: functions using
884      * a DOM node's JavaScript object should not have to check if they should create it first.</p>
885      *
886      * @param <T> the object type
887      * @return the JavaScript object that corresponds to this node
888      */
889     @SuppressWarnings("unchecked")
890     public <T extends HtmlUnitScriptable> T getScriptableObject() {
891         if (scriptObject_ == null) {
892             final SgmlPage page = getPage();
893             if (this == page) {
894                 final StringBuilder msg = new StringBuilder("No script object associated with the Page.");
895                 // because this is a strange case we like to provide as much info as possible
896                 msg.append(" class: '")
897                     .append(page.getClass().getName())
898                     .append('\'');
899                 try {
900                     msg.append(" url: '")
901                         .append(page.getUrl()).append("' content: ")
902                         .append(page.getWebResponse().getContentAsString());
903                 }
904                 catch (final Exception e) {
905                     // ok bad luck with detail
906                     msg.append(" no details: '").append(e).append('\'');
907                 }
908                 throw new IllegalStateException(msg.toString());
909             }
910             scriptObject_ = page.getScriptableObject().makeScriptableFor(this);
911         }
912         return (T) scriptObject_;
913     }
914 
915     /**
916      * {@inheritDoc}
917      */
918     @Override
919     public DomNode appendChild(final Node node) {
920         if (node == this) {
921             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add not to itself " + this);
922         }
923         final DomNode domNode = (DomNode) node;
924         if (domNode.isAncestorOf(this)) {
925             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add (grand)parent to itself " + this);
926         }
927 
928         if (domNode instanceof DomDocumentFragment fragment) {
929             for (final DomNode child : fragment.getChildren()) {
930                 appendChild(child);
931             }
932         }
933         else {
934             // clean up the new node, in case it is being moved
935             if (domNode.getParentNode() != null) {
936                 domNode.detach();
937             }
938 
939             basicAppend(domNode);
940 
941             fireAddition(domNode);
942         }
943 
944         return domNode;
945     }
946 
947     /**
948      * Appends the specified node to the end of this node's children, assuming the specified
949      * node is clean (doesn't have preexisting relationships to other nodes).
950      *
951      * @param node the node to append to this node's children
952      */
953     private void basicAppend(final DomNode node) {
954         // try to make the node setup as complete as possible
955         // before the node is reachable
956         node.setPage(getPage());
957         node.parent_ = this;
958 
959         if (firstChild_ == null) {
960             firstChild_ = node;
961         }
962         else {
963             final DomNode last = getLastChild();
964             node.previousSibling_ = last;
965             node.nextSibling_ = null; // safety first
966 
967             last.nextSibling_ = node;
968         }
969         firstChild_.previousSibling_ = node;
970     }
971 
972     /**
973      * {@inheritDoc}
974      */
975     @Override
976     public Node insertBefore(final Node newChild, final Node refChild) {
977         if (newChild instanceof DomDocumentFragment fragment) {
978             for (final DomNode child : fragment.getChildren()) {
979                 insertBefore(child, refChild);
980             }
981             return newChild;
982         }
983 
984         if (refChild == null) {
985             appendChild(newChild);
986             return newChild;
987         }
988 
989         if (refChild.getParentNode() != this) {
990             throw new DOMException(DOMException.NOT_FOUND_ERR, "Reference node is not a child of this node.");
991         }
992 
993         ((DomNode) refChild).insertBefore((DomNode) newChild);
994         return newChild;
995     }
996 
997     /**
998      * Inserts the specified node as a new child node before this node into the child relationship this node is a
999      * part of. If the specified node is this node, this method is a no-op.
1000      *
1001      * @param newNode the new node to insert
1002      */
1003     public void insertBefore(final DomNode newNode) {
1004         if (previousSibling_ == null) {
1005             throw new IllegalStateException("Previous sibling for " + this + " is null.");
1006         }
1007 
1008         if (newNode == this) {
1009             return;
1010         }
1011 
1012         if (newNode instanceof DomDocumentFragment) {
1013             for (final DomNode child : newNode.getChildren()) {
1014                 insertBefore(child);
1015             }
1016             return;
1017         }
1018 
1019         // clean up the new node, in case it is being moved
1020         if (newNode.getParentNode() != null) {
1021             newNode.detach();
1022         }
1023 
1024         basicInsertBefore(newNode);
1025 
1026         fireAddition(newNode);
1027     }
1028 
1029     /**
1030      * Inserts the specified node into this node's parent's children right before this node, assuming the specified
1031      * node is clean (doesn't have preexisting relationships to other nodes).
1032      *
1033      * @param node the node to insert before this node
1034      */
1035     private void basicInsertBefore(final DomNode node) {
1036         // try to make the node setup as complete as possible
1037         // before the node is reachable
1038         node.setPage(page_);
1039         node.parent_ = parent_;
1040         node.previousSibling_ = previousSibling_;
1041         node.nextSibling_ = this;
1042 
1043         if (parent_.firstChild_ == this) {
1044             parent_.firstChild_ = node;
1045         }
1046         else {
1047             previousSibling_.nextSibling_ = node;
1048         }
1049         previousSibling_ = node;
1050     }
1051 
1052     private void fireAddition(final DomNode domNode) {
1053         final boolean wasAlreadyAttached = domNode.isAttachedToPage();
1054         domNode.attachedToPage_ = isAttachedToPage();
1055 
1056         final SgmlPage page = getPage();
1057         if (domNode.attachedToPage_) {
1058             // trigger events
1059             if (null != page && page.isHtmlPage()) {
1060                 ((HtmlPage) page).notifyNodeAdded(domNode);
1061             }
1062 
1063             // a node that is already "complete" (ie not being parsed) and not yet attached
1064             if (!domNode.isBodyParsed() && !wasAlreadyAttached) {
1065                 if (domNode.getFirstChild() != null) {
1066                     for (final Iterator<DomNode> iterator =
1067                             domNode.new DescendantDomNodesIterator(); iterator.hasNext();) {
1068                         final DomNode child = iterator.next();
1069                         child.attachedToPage_ = true;
1070                         child.onAllChildrenAddedToPage(true);
1071                     }
1072                 }
1073                 domNode.onAllChildrenAddedToPage(true);
1074             }
1075         }
1076 
1077         if (this instanceof DomDocumentFragment) {
1078             onAddedToDocumentFragment();
1079         }
1080 
1081         if (page == null || page.isDomChangeListenerInUse()) {
1082             fireNodeAdded(this, domNode);
1083         }
1084     }
1085 
1086     /**
1087      * Indicates if the current node is being parsed. This means that the opening tag has already been
1088      * parsed but not the body and end tag.
1089      */
1090     private boolean isBodyParsed() {
1091         return getStartLineNumber() != -1 && getEndLineNumber() == -1;
1092     }
1093 
1094     /**
1095      * Recursively sets the new page on the node and its children
1096      * @param newPage the new owning page
1097      */
1098     private void setPage(final SgmlPage newPage) {
1099         if (page_ == newPage) {
1100             return; // nothing to do
1101         }
1102 
1103         page_ = newPage;
1104         for (final DomNode node : getChildren()) {
1105             node.setPage(newPage);
1106         }
1107     }
1108 
1109     /**
1110      * {@inheritDoc}
1111      */
1112     @Override
1113     public Node removeChild(final Node child) {
1114         if (child.getParentNode() != this) {
1115             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1116         }
1117         ((DomNode) child).remove();
1118         return child;
1119     }
1120 
1121     /**
1122      * Removes all of this node's children.
1123      */
1124     public void removeAllChildren() {
1125         while (getFirstChild() != null) {
1126             getFirstChild().remove();
1127         }
1128     }
1129 
1130     /**
1131      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1132      *
1133      * Parses the specified HTML source code, appending the resulting content at the specified target location.
1134      * @param source the HTML code extract to parse
1135      * @throws IOException in case of error
1136      * @throws SAXException in case of error
1137      */
1138     public void parseHtmlSnippet(final String source) throws SAXException, IOException {
1139         final WebClient webClient = getPage().getWebClient();
1140         webClient.getPageCreator().getHtmlParser().parseFragment(webClient, this, this, source, false);
1141     }
1142 
1143     /**
1144      * Removes this node from all relationships with other nodes.
1145      */
1146     public void remove() {
1147         // same as detach for the moment
1148         detach();
1149     }
1150 
1151     /**
1152      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1153      *
1154      * Detach this node from all relationships with other nodes.
1155      * This is the first step of a move.
1156      */
1157     protected void detach() {
1158         final DomNode exParent = parent_;
1159 
1160         basicRemove();
1161 
1162         fireRemoval(exParent);
1163     }
1164 
1165     /**
1166      * Cuts off all relationships this node has with siblings and parents.
1167      */
1168     protected void basicRemove() {
1169         basicDetach();
1170 
1171         nextSibling_ = null;
1172         previousSibling_ = null;
1173         parent_ = null;
1174         attachedToPage_ = false;
1175         for (final DomNode descendant : getDescendants()) {
1176             descendant.attachedToPage_ = false;
1177         }
1178     }
1179 
1180     /**
1181      * Cuts off all relationships this node has with siblings and parents.
1182      */
1183     private void basicDetach() {
1184         if (parent_ != null && parent_.firstChild_ == this) {
1185             parent_.firstChild_ = nextSibling_;
1186         }
1187         else if (previousSibling_ != null && previousSibling_.nextSibling_ == this) {
1188             previousSibling_.nextSibling_ = nextSibling_;
1189         }
1190         if (nextSibling_ != null && nextSibling_.previousSibling_ == this) {
1191             nextSibling_.previousSibling_ = previousSibling_;
1192         }
1193         if (parent_ != null && parent_.getLastChild() == this) {
1194             parent_.firstChild_.previousSibling_ = previousSibling_;
1195         }
1196     }
1197 
1198     private void fireRemoval(final DomNode exParent) {
1199         final SgmlPage page = getPage();
1200         if (page instanceof HtmlPage htmlPage) {
1201             // some actions executed on removal need an intact parent relationship (e.g. for the
1202             // DocumentPositionComparator) so we have to restore it temporarily
1203             parent_ = exParent;
1204             htmlPage.notifyNodeRemoved(this);
1205             parent_ = null;
1206         }
1207 
1208         if (exParent != null && (page == null || page.isDomChangeListenerInUse())) {
1209             fireNodeDeleted(exParent, this);
1210             // ask ex-parent to fire event (because we don't have parent now)
1211             exParent.fireNodeDeleted(exParent, this);
1212         }
1213     }
1214 
1215     /**
1216      * {@inheritDoc}
1217      */
1218     @Override
1219     public Node replaceChild(final Node newChild, final Node oldChild) {
1220         if (oldChild.getParentNode() != this) {
1221             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1222         }
1223         ((DomNode) oldChild).replace((DomNode) newChild);
1224         return oldChild;
1225     }
1226 
1227     /**
1228      * Replaces this node with another node. If the specified node is this node, this
1229      * method is a no-op.
1230      * @param newNode the node to replace this one
1231      */
1232     public void replace(final DomNode newNode) {
1233         if (newNode != this) {
1234             final DomNode exParent = parent_;
1235             final DomNode exNextSibling = nextSibling_;
1236 
1237             remove();
1238 
1239             exParent.insertBefore(newNode, exNextSibling);
1240         }
1241     }
1242 
1243     /**
1244      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1245      *
1246      * Quietly removes this node and moves its children to the specified destination. "Quietly" means
1247      * that no node events are fired. This method is not appropriate for most use cases. It should
1248      * only be used in specific cases for HTML parsing hackery.
1249      *
1250      * @param destination the node to which this node's children should be moved before this node is removed
1251      */
1252     public void quietlyRemoveAndMoveChildrenTo(final DomNode destination) {
1253         if (destination.getPage() != getPage()) {
1254             throw new RuntimeException("Cannot perform quiet move on nodes from different pages.");
1255         }
1256         for (final DomNode child : getChildren()) {
1257             if (child != destination) {
1258                 child.basicRemove();
1259                 destination.basicAppend(child);
1260             }
1261         }
1262         basicRemove();
1263     }
1264 
1265     /**
1266      * Check for insertion errors for a new child node. This is overridden by derived
1267      * classes to enforce which types of children are allowed.
1268      *
1269      * @param newChild the new child node that is being inserted below this node
1270      * @throws DOMException HIERARCHY_REQUEST_ERR: Raised if this node is of a type that does
1271      *         not allow children of the type of the newChild node, or if the node to insert is one of
1272      *         this node's ancestors or this node itself, or if this node is of type Document and the
1273      *         DOM application attempts to insert a second DocumentType or Element node.
1274      *         WRONG_DOCUMENT_ERR: Raised if newChild was created from a different document than the
1275      *         one that created this node.
1276      */
1277     protected void checkChildHierarchy(final Node newChild) throws DOMException {
1278         Node parentNode = this;
1279         while (parentNode != null) {
1280             if (parentNode == newChild) {
1281                 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Child node is already a parent.");
1282             }
1283             parentNode = parentNode.getParentNode();
1284         }
1285         final Document thisDocument = getOwnerDocument();
1286         final Document childDocument = newChild.getOwnerDocument();
1287         if (childDocument != thisDocument && childDocument != null) {
1288             throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Child node " + newChild.getNodeName()
1289                 + " is not in the same Document as this " + getNodeName() + ".");
1290         }
1291     }
1292 
1293     /**
1294      * Lifecycle method invoked whenever a node is added to a page. Intended to
1295      * be overridden by nodes which need to perform custom logic when they are
1296      * added to a page. This method is recursive, so if you override it, please
1297      * be sure to call <code>super.onAddedToPage()</code>.
1298      */
1299     protected void onAddedToPage() {
1300         if (firstChild_ != null) {
1301             for (final DomNode child : getChildren()) {
1302                 child.onAddedToPage();
1303             }
1304         }
1305     }
1306 
1307     /**
1308      * Lifecycle method invoked after a node and all its children have been added to a page, during
1309      * parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic
1310      * after they and all their child nodes have been processed by the HTML parser. This method is
1311      * not recursive, and the default implementation is empty, so there is no need to call
1312      * <code>super.onAllChildrenAddedToPage()</code> if you implement this method.
1313      * @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no
1314      */
1315     public void onAllChildrenAddedToPage(final boolean postponed) {
1316         // Empty by default.
1317     }
1318 
1319     /**
1320      * Lifecycle method invoked whenever a node is added to a document fragment. Intended to
1321      * be overridden by nodes which need to perform custom logic when they are
1322      * added to a fragment. This method is recursive, so if you override it, please
1323      * be sure to call <code>super.onAddedToDocumentFragment()</code>.
1324      */
1325     protected void onAddedToDocumentFragment() {
1326         if (firstChild_ != null) {
1327             for (final DomNode child : getChildren()) {
1328                 child.onAddedToDocumentFragment();
1329             }
1330         }
1331     }
1332 
1333     /**
1334      * Add a DOM node as a child to this node before the referenced node.
1335      * If the referenced node is null, append to the end.
1336      * @param movedDomNode the node to move
1337      * @param referenceDomNode the node to move before
1338      * @throws DOMException in case of problems
1339      */
1340     public void moveBefore(final DomNode movedDomNode, final DomNode referenceDomNode) {
1341         if (movedDomNode == referenceDomNode) {
1342             return;
1343         }
1344 
1345         if (movedDomNode instanceof DomDocumentFragment fragment) {
1346             for (final DomNode child : fragment.getChildren()) {
1347                 moveBefore(child, referenceDomNode);
1348             }
1349             return;
1350         }
1351 
1352         // If moving to the same position (node is already right before referenceNode), no operation needed
1353         if (referenceDomNode != null && movedDomNode.getNextSibling() == referenceDomNode) {
1354             return;
1355         }
1356 
1357         if (movedDomNode.isAncestorOf(this)) {
1358             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1359                     "The new child element contains the parent.");
1360         }
1361 
1362         if (referenceDomNode != null && !this.isAncestorOf(referenceDomNode)) {
1363             throw new DOMException(DOMException.NOT_FOUND_ERR,
1364                     "The node before which the new node is to be inserted is not a child of this node.");
1365         }
1366 
1367         if (referenceDomNode != null && referenceDomNode.isAttachedToPage() && !movedDomNode.isAttachedToPage()) {
1368             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1369                     "State-preserving atomic move cannot be performed on nodes participating in an invalid hierarchy.");
1370         }
1371 
1372         if (referenceDomNode == null) {
1373             appendChild(movedDomNode);
1374             return;
1375         }
1376 
1377         referenceDomNode.moveBefore(movedDomNode);
1378     }
1379 
1380     /**
1381      * Inserts the specified node as a new child node before this node into the child relationship this node is a
1382      * part of. If the specified node is this node, this method is a no-op.
1383      *
1384      * @param movedDomNode the node to move before the current node
1385      */
1386     public void moveBefore(final DomNode movedDomNode) {
1387         if (previousSibling_ == null) {
1388             throw new IllegalStateException("Previous sibling for " + this + " is null.");
1389         }
1390 
1391         if (movedDomNode == this) {
1392             return;
1393         }
1394 
1395         movedDomNode.detach();
1396         basicInsertBefore(movedDomNode);
1397 
1398         fireAddition(movedDomNode);
1399     }
1400 
1401     /**
1402      * @return an {@link Iterable} over the children of this node
1403      */
1404     public final Iterable<DomNode> getChildren() {
1405         return () -> new ChildIterator(firstChild_);
1406     }
1407 
1408     /**
1409      * An iterator over all children of this node.
1410      */
1411     protected static class ChildIterator implements Iterator<DomNode> {
1412 
1413         private DomNode nextNode_;
1414         private DomNode currentNode_;
1415 
1416         public ChildIterator(final DomNode nextNode) {
1417             nextNode_ = nextNode;
1418         }
1419 
1420         /** {@inheritDoc} */
1421         @Override
1422         public boolean hasNext() {
1423             return nextNode_ != null;
1424         }
1425 
1426         /** {@inheritDoc} */
1427         @Override
1428         public DomNode next() {
1429             if (nextNode_ != null) {
1430                 currentNode_ = nextNode_;
1431                 nextNode_ = nextNode_.nextSibling_;
1432                 return currentNode_;
1433             }
1434             throw new NoSuchElementException();
1435         }
1436 
1437         /** {@inheritDoc} */
1438         @Override
1439         public void remove() {
1440             if (currentNode_ == null) {
1441                 throw new IllegalStateException();
1442             }
1443             currentNode_.remove();
1444         }
1445     }
1446 
1447     /**
1448      * Returns an {@link Iterable} that will recursively iterate over all of this node's descendants,
1449      * including {@link DomText} elements, {@link DomComment} elements, etc. If you want to iterate
1450      * only over {@link HtmlElement} descendants, please use {@link #getHtmlElementDescendants()}.
1451      * @return an {@link Iterable} that will recursively iterate over all of this node's descendants
1452      */
1453     public final Iterable<DomNode> getDescendants() {
1454         return () -> new DescendantDomNodesIterator();
1455     }
1456 
1457     /**
1458      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1459      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1460      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1461      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1462      *         descendants
1463      * @see #getDomElementDescendants()
1464      */
1465     public final Iterable<HtmlElement> getHtmlElementDescendants() {
1466         return () -> new DescendantHtmlElementsIterator();
1467     }
1468 
1469     /**
1470      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1471      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1472      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1473      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1474      *         descendants
1475      * @see #getHtmlElementDescendants()
1476      */
1477     public final Iterable<DomElement> getDomElementDescendants() {
1478         return () -> new DescendantDomElementsIterator();
1479     }
1480 
1481     /**
1482      * Iterates over all descendants DomNodes, in document order.
1483      */
1484     protected final class DescendantDomNodesIterator implements Iterator<DomNode> {
1485         private DomNode currentNode_;
1486         private DomNode nextNode_;
1487 
1488         /**
1489          * Creates a new instance which iterates over the specified node type.
1490          */
1491         public DescendantDomNodesIterator() {
1492             nextNode_ = DomNode.this.getFirstChild();
1493         }
1494 
1495         /** {@inheritDoc} */
1496         @Override
1497         public boolean hasNext() {
1498             return nextNode_ != null;
1499         }
1500 
1501         /** {@inheritDoc} */
1502         @Override
1503         public DomNode next() {
1504             return nextNode();
1505         }
1506 
1507         /** {@inheritDoc} */
1508         @Override
1509         public void remove() {
1510             if (currentNode_ == null) {
1511                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1512             }
1513             final DomNode current = currentNode_;
1514             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1515                 next();
1516             }
1517             current.remove();
1518         }
1519 
1520         /**
1521          * @return the next node, if there is one
1522          */
1523         public DomNode nextNode() {
1524             currentNode_ = nextNode_;
1525 
1526             DomNode next = nextNode_.getFirstChild();
1527             if (next == null) {
1528                 next = nextNode_.getNextSibling();
1529             }
1530             if (next == null) {
1531                 next = getNextElementUpwards(nextNode_);
1532             }
1533             nextNode_ = next;
1534 
1535             return currentNode_;
1536         }
1537 
1538         private DomNode getNextElementUpwards(final DomNode startingNode) {
1539             if (startingNode == DomNode.this) {
1540                 return null;
1541             }
1542 
1543             DomNode parent = startingNode.getParentNode();
1544             while (parent != null && parent != DomNode.this) {
1545                 final DomNode next = parent.getNextSibling();
1546                 if (next != null) {
1547                     return next;
1548                 }
1549                 parent = parent.getParentNode();
1550             }
1551             return null;
1552         }
1553     }
1554 
1555     /**
1556      * Iterates over all descendants DomTypes, in document order.
1557      */
1558     protected final class DescendantDomElementsIterator implements Iterator<DomElement> {
1559         private DomNode currentNode_;
1560         private DomNode nextNode_;
1561 
1562         /**
1563          * Creates a new instance which iterates over the specified node type.
1564          */
1565         public DescendantDomElementsIterator() {
1566             nextNode_ = getFirstChildElement(DomNode.this);
1567         }
1568 
1569         /** {@inheritDoc} */
1570         @Override
1571         public boolean hasNext() {
1572             return nextNode_ != null;
1573         }
1574 
1575         /** {@inheritDoc} */
1576         @Override
1577         public DomElement next() {
1578             return nextNode();
1579         }
1580 
1581         /** {@inheritDoc} */
1582         @Override
1583         public void remove() {
1584             if (currentNode_ == null) {
1585                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1586             }
1587             final DomNode current = currentNode_;
1588             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1589                 next();
1590             }
1591             current.remove();
1592         }
1593 
1594         /**
1595          * @return the next node, if there is one
1596          */
1597         public DomElement nextNode() {
1598             currentNode_ = nextNode_;
1599 
1600             DomNode next = getFirstChildElement(nextNode_);
1601             if (next == null) {
1602                 next = getNextDomSibling(nextNode_);
1603             }
1604             if (next == null) {
1605                 next = getNextElementUpwards(nextNode_);
1606             }
1607             nextNode_ = next;
1608 
1609             return (DomElement) currentNode_;
1610         }
1611 
1612         private DomNode getNextElementUpwards(final DomNode startingNode) {
1613             if (startingNode == DomNode.this) {
1614                 return null;
1615             }
1616 
1617             DomNode parent = startingNode.getParentNode();
1618             while (parent != null && parent != DomNode.this) {
1619                 DomNode next = parent.getNextSibling();
1620                 while (next != null && !isAccepted(next)) {
1621                     next = next.getNextSibling();
1622                 }
1623                 if (next != null) {
1624                     return next;
1625                 }
1626                 parent = parent.getParentNode();
1627             }
1628             return null;
1629         }
1630 
1631         private DomNode getFirstChildElement(final DomNode parent) {
1632             DomNode node = parent.getFirstChild();
1633             while (node != null && !isAccepted(node)) {
1634                 node = node.getNextSibling();
1635             }
1636             return node;
1637         }
1638 
1639         /**
1640          * Indicates if the node is accepted. If not it won't be explored at all.
1641          * @param node the node to test
1642          * @return {@code true} if accepted
1643          */
1644         private boolean isAccepted(final DomNode node) {
1645             return DomElement.class.isAssignableFrom(node.getClass());
1646         }
1647 
1648         private DomNode getNextDomSibling(final DomNode element) {
1649             DomNode node = element.getNextSibling();
1650             while (node != null && !isAccepted(node)) {
1651                 node = node.getNextSibling();
1652             }
1653             return node;
1654         }
1655     }
1656 
1657     /**
1658      * Iterates over all descendants HtmlElements, in document order.
1659      */
1660     protected final class DescendantHtmlElementsIterator implements Iterator<HtmlElement> {
1661         private DomNode currentNode_;
1662         private DomNode nextNode_;
1663 
1664         /**
1665          * Creates a new instance which iterates over the specified node type.
1666          */
1667         public DescendantHtmlElementsIterator() {
1668             nextNode_ = getFirstChildElement(DomNode.this);
1669         }
1670 
1671         /** {@inheritDoc} */
1672         @Override
1673         public boolean hasNext() {
1674             return nextNode_ != null;
1675         }
1676 
1677         /** {@inheritDoc} */
1678         @Override
1679         public HtmlElement next() {
1680             return nextNode();
1681         }
1682 
1683         /** {@inheritDoc} */
1684         @Override
1685         public void remove() {
1686             if (currentNode_ == null) {
1687                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1688             }
1689             final DomNode current = currentNode_;
1690             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1691                 next();
1692             }
1693             current.remove();
1694         }
1695 
1696         /**
1697          * @return the next node, if there is one
1698          */
1699         public HtmlElement nextNode() {
1700             currentNode_ = nextNode_;
1701 
1702             DomNode next = getFirstChildElement(nextNode_);
1703             if (next == null) {
1704                 next = getNextDomSibling(nextNode_);
1705             }
1706             if (next == null) {
1707                 next = getNextElementUpwards(nextNode_);
1708             }
1709             nextNode_ = next;
1710 
1711             return (HtmlElement) currentNode_;
1712         }
1713 
1714         private DomNode getNextElementUpwards(final DomNode startingNode) {
1715             if (startingNode == DomNode.this) {
1716                 return null;
1717             }
1718 
1719             DomNode parent = startingNode.getParentNode();
1720             while (parent != null && parent != DomNode.this) {
1721                 DomNode next = parent.getNextSibling();
1722                 while (next != null && !isAccepted(next)) {
1723                     next = next.getNextSibling();
1724                 }
1725                 if (next != null) {
1726                     return next;
1727                 }
1728                 parent = parent.getParentNode();
1729             }
1730             return null;
1731         }
1732 
1733         private DomNode getFirstChildElement(final DomNode parent) {
1734             DomNode node = parent.getFirstChild();
1735             while (node != null && !isAccepted(node)) {
1736                 node = node.getNextSibling();
1737             }
1738             return node;
1739         }
1740 
1741         /**
1742          * Indicates if the node is accepted. If not it won't be explored at all.
1743          * @param node the node to test
1744          * @return {@code true} if accepted
1745          */
1746         private boolean isAccepted(final DomNode node) {
1747             return HtmlElement.class.isAssignableFrom(node.getClass());
1748         }
1749 
1750         private DomNode getNextDomSibling(final DomNode element) {
1751             DomNode node = element.getNextSibling();
1752             while (node != null && !isAccepted(node)) {
1753                 node = node.getNextSibling();
1754             }
1755             return node;
1756         }
1757     }
1758 
1759     /**
1760      * Returns this node's ready state (IE only).
1761      * @return this node's ready state
1762      */
1763     public String getReadyState() {
1764         return readyState_;
1765     }
1766 
1767     /**
1768      * Sets this node's ready state (IE only).
1769      * @param state this node's ready state
1770      */
1771     public void setReadyState(final String state) {
1772         readyState_ = state;
1773     }
1774 
1775     /**
1776      * Evaluates the specified XPath expression from this node, returning the matching elements.
1777      * <br>
1778      * Note: This implies that the ',' point to this node but the general axis like '//' are still
1779      * looking at the whole document. E.g. if you like to get all child h1 nodes from the current one
1780      * you have to use './/h1' instead of '//h1' because the latter matches all h1 nodes of the#
1781      * whole document.
1782      *
1783      * @param <T> the expected type
1784      * @param xpathExpr the XPath expression to evaluate
1785      * @return the elements which match the specified XPath expression
1786      * @see #getFirstByXPath(String)
1787      * @see #getCanonicalXPath()
1788      */
1789     public <T> List<T> getByXPath(final String xpathExpr) {
1790         return XPathHelper.getByXPath(this, xpathExpr, null);
1791     }
1792 
1793     /**
1794      * Evaluates the specified XPath expression from this node, returning the matching elements.
1795      *
1796      * @param xpathExpr the XPath expression to evaluate
1797      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1798      * @return the elements which match the specified XPath expression
1799      * @see #getFirstByXPath(String)
1800      * @see #getCanonicalXPath()
1801      */
1802     public List<?> getByXPath(final String xpathExpr, final PrefixResolver resolver) {
1803         return XPathHelper.getByXPath(this, xpathExpr, resolver);
1804     }
1805 
1806     /**
1807      * Evaluates the specified XPath expression from this node, returning the first matching element,
1808      * or {@code null} if no node matches the specified XPath expression.
1809      *
1810      * @param xpathExpr the XPath expression
1811      * @param <X> the expression type
1812      * @return the first element matching the specified XPath expression
1813      * @see #getByXPath(String)
1814      * @see #getCanonicalXPath()
1815      */
1816     public <X> X getFirstByXPath(final String xpathExpr) {
1817         return getFirstByXPath(xpathExpr, null);
1818     }
1819 
1820     /**
1821      * Evaluates the specified XPath expression from this node, returning the first matching element,
1822      * or {@code null} if no node matches the specified XPath expression.
1823      *
1824      * @param xpathExpr the XPath expression
1825      * @param <X> the expression type
1826      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1827      * @return the first element matching the specified XPath expression
1828      * @see #getByXPath(String)
1829      * @see #getCanonicalXPath()
1830      */
1831     @SuppressWarnings("unchecked")
1832     public <X> X getFirstByXPath(final String xpathExpr, final PrefixResolver resolver) {
1833         final List<?> results = getByXPath(xpathExpr, resolver);
1834         if (results.isEmpty()) {
1835             return null;
1836         }
1837         return (X) results.get(0);
1838     }
1839 
1840     /**
1841      * <p>Returns the canonical XPath expression which identifies this node, for instance
1842      * <code>"/html/body/table[3]/tbody/tr[5]/td[2]/span/a[3]"</code>.</p>
1843      *
1844      * <p><span style="color:red">WARNING:</span> This sort of automated XPath expression
1845      * is often quite bad at identifying a node, as it is highly sensitive to changes in
1846      * the DOM tree.</p>
1847      *
1848      * @return the canonical XPath expression which identifies this node
1849      * @see #getByXPath(String)
1850      */
1851     public String getCanonicalXPath() {
1852         throw new RuntimeException("Method getCanonicalXPath() not implemented for nodes of type " + getNodeType());
1853     }
1854 
1855     /**
1856      * Notifies the registered {@link IncorrectnessListener} of something that is not fully correct.
1857      * @param message the notification to send to the registered {@link IncorrectnessListener}
1858      */
1859     protected void notifyIncorrectness(final String message) {
1860         final WebClient client = getPage().getEnclosingWindow().getWebClient();
1861         final IncorrectnessListener incorrectnessListener = client.getIncorrectnessListener();
1862         incorrectnessListener.notify(message, this);
1863     }
1864 
1865     /**
1866      * Adds a {@link DomChangeListener} to the listener list. The listener is registered for
1867      * all descendants of this node.
1868      *
1869      * @param listener the DOM structure change listener to be added
1870      * @see #removeDomChangeListener(DomChangeListener)
1871      */
1872     public void addDomChangeListener(final DomChangeListener listener) {
1873         WebAssert.notNull("listener", listener);
1874 
1875         synchronized (this) {
1876             if (domListeners_ == null) {
1877                 domListeners_ = new ArrayList<>();
1878             }
1879             domListeners_.add(listener);
1880 
1881             final SgmlPage page = getPage();
1882             if (page != null) {
1883                 page.domChangeListenerAdded();
1884             }
1885         }
1886     }
1887 
1888     /**
1889      * Removes a {@link DomChangeListener} from the listener list. The listener is deregistered for
1890      * all descendants of this node.
1891      *
1892      * @param listener the DOM structure change listener to be removed
1893      * @see #addDomChangeListener(DomChangeListener)
1894      */
1895     public void removeDomChangeListener(final DomChangeListener listener) {
1896         WebAssert.notNull("listener", listener);
1897 
1898         synchronized (this) {
1899             if (domListeners_ != null) {
1900                 domListeners_.remove(listener);
1901             }
1902         }
1903     }
1904 
1905     /**
1906      * Support for reporting DOM changes. This method can be called when a node has been added, and it
1907      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
1908      *
1909      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeAdded(DomNode, DomNode)}.</p>
1910      *
1911      * @param parentNode the parent of the node that was changed
1912      * @param addedNode the node that has been added
1913      */
1914     protected void fireNodeAdded(final DomNode parentNode, final DomNode addedNode) {
1915         DomChangeEvent event = null;
1916 
1917         DomNode toInform = this;
1918         while (toInform != null) {
1919             if (toInform.domListeners_ != null) {
1920                 final List<DomChangeListener> listeners;
1921                 synchronized (toInform) {
1922                     listeners = new ArrayList<>(toInform.domListeners_);
1923                 }
1924 
1925                 if (event == null) {
1926                     event = new DomChangeEvent(parentNode, addedNode);
1927                 }
1928                 for (final DomChangeListener domChangeListener : listeners) {
1929                     domChangeListener.nodeAdded(event);
1930                 }
1931             }
1932 
1933             toInform = toInform.getParentNode();
1934         }
1935     }
1936 
1937     /**
1938      * Adds a {@link CharacterDataChangeListener} to the listener list. The listener is registered for
1939      * all descendants of this node.
1940      *
1941      * @param listener the character data change listener to be added
1942      * @see #removeCharacterDataChangeListener(CharacterDataChangeListener)
1943      */
1944     public void addCharacterDataChangeListener(final CharacterDataChangeListener listener) {
1945         WebAssert.notNull("listener", listener);
1946 
1947         synchronized (this) {
1948             if (characterDataListeners_ == null) {
1949                 characterDataListeners_ = new ArrayList<>();
1950             }
1951             characterDataListeners_.add(listener);
1952 
1953             final SgmlPage page = getPage();
1954             if (page != null) {
1955                 page.characterDataChangeListenerAdded();
1956             }
1957         }
1958     }
1959 
1960     /**
1961      * Removes a {@link CharacterDataChangeListener} from the listener list. The listener is deregistered for
1962      * all descendants of this node.
1963      *
1964      * @param listener the Character Data change listener to be removed
1965      * @see #addCharacterDataChangeListener(CharacterDataChangeListener)
1966      */
1967     public void removeCharacterDataChangeListener(final CharacterDataChangeListener listener) {
1968         WebAssert.notNull("listener", listener);
1969 
1970         synchronized (this) {
1971             if (characterDataListeners_ != null) {
1972                 characterDataListeners_.remove(listener);
1973             }
1974         }
1975     }
1976 
1977     /**
1978      * Support for reporting Character Data changes.
1979      *
1980      * <p>Note that this method recursively calls this node's parent's {@link #fireCharacterDataChanged}.</p>
1981      *
1982      * @param characterData the character data which is changed
1983      * @param oldValue the old value
1984      */
1985     protected void fireCharacterDataChanged(final DomCharacterData characterData, final String oldValue) {
1986         CharacterDataChangeEvent event = null;
1987 
1988         DomNode toInform = this;
1989         while (toInform != null) {
1990             if (toInform.characterDataListeners_ != null) {
1991                 final List<CharacterDataChangeListener> listeners;
1992                 synchronized (toInform) {
1993                     listeners = new ArrayList<>(toInform.characterDataListeners_);
1994                 }
1995 
1996                 if (event == null) {
1997                     event = new CharacterDataChangeEvent(characterData, oldValue);
1998                 }
1999                 for (final CharacterDataChangeListener domChangeListener : listeners) {
2000                     domChangeListener.characterDataChanged(event);
2001                 }
2002             }
2003 
2004             toInform = toInform.getParentNode();
2005         }
2006     }
2007 
2008     /**
2009      * Support for reporting DOM changes. This method can be called when a node has been deleted, and it
2010      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
2011      *
2012      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeDeleted(DomNode, DomNode)}.</p>
2013      *
2014      * @param parentNode the parent of the node that was changed
2015      * @param deletedNode the node that has been deleted
2016      */
2017     protected void fireNodeDeleted(final DomNode parentNode, final DomNode deletedNode) {
2018         DomChangeEvent event = null;
2019 
2020         DomNode toInform = this;
2021         while (toInform != null) {
2022             if (toInform.domListeners_ != null) {
2023                 final List<DomChangeListener> listeners;
2024                 synchronized (toInform) {
2025                     listeners = new ArrayList<>(toInform.domListeners_);
2026                 }
2027 
2028                 if (event == null) {
2029                     event = new DomChangeEvent(parentNode, deletedNode);
2030                 }
2031                 for (final DomChangeListener domChangeListener : listeners) {
2032                     domChangeListener.nodeDeleted(event);
2033                 }
2034             }
2035 
2036             toInform = toInform.getParentNode();
2037         }
2038     }
2039 
2040     /**
2041      * Retrieves all element nodes from descendants of the starting element node that match any selector
2042      * within the supplied selector strings.
2043      * @param selectors one or more CSS selectors separated by commas
2044      * @return list of all found nodes
2045      */
2046     public DomNodeList<DomNode> querySelectorAll(final String selectors) {
2047         try {
2048             final WebClient webClient = getPage().getWebClient();
2049             final SelectorList selectorList = getSelectorList(selectors, webClient);
2050 
2051             final List<DomNode> elements = new ArrayList<>();
2052             if (selectorList != null) {
2053                 for (final DomElement child : getDomElementDescendants()) {
2054                     for (final Selector selector : selectorList) {
2055                         if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, child, null, true, true)) {
2056                             elements.add(child);
2057                             break;
2058                         }
2059                     }
2060                 }
2061             }
2062             return new StaticDomNodeList(elements);
2063         }
2064         catch (final IOException e) {
2065             throw new CSSException("Error parsing CSS selectors from '" + selectors + "': " + e.getMessage(), e);
2066         }
2067     }
2068 
2069     /**
2070      * Returns the {@link SelectorList}.
2071      * @param selectors the selectors
2072      * @param webClient the {@link WebClient}
2073      * @return the {@link SelectorList}
2074      * @throws IOException if an error occurs
2075      */
2076     protected SelectorList getSelectorList(final String selectors, final WebClient webClient)
2077             throws IOException {
2078 
2079         // get us a CSS3Parser from the pool so the chance of reusing it are high
2080         try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
2081             final CSSOMParser parser = new CSSOMParser(pooledParser);
2082             final CheckErrorHandler errorHandler = new CheckErrorHandler();
2083             parser.setErrorHandler(errorHandler);
2084 
2085             final SelectorList selectorList = parser.parseSelectors(selectors);
2086             // in case of error parseSelectors returns null
2087             if (errorHandler.error() != null) {
2088                 throw new CSSException("Invalid selectors: '" + selectors + "'", errorHandler.error());
2089             }
2090 
2091             if (selectorList != null) {
2092                 CssStyleSheet.validateSelectors(selectorList, this);
2093 
2094             }
2095             return selectorList;
2096         }
2097     }
2098 
2099     /**
2100      * Returns the first element within the document that matches the specified group of selectors.
2101      * @param selectors one or more CSS selectors separated by commas
2102      * @param <N> the node type
2103      * @return null if no matches are found; otherwise, it returns the first matching element
2104      */
2105     @SuppressWarnings("unchecked")
2106     public <N extends DomNode> N querySelector(final String selectors) {
2107         final DomNodeList<DomNode> list = querySelectorAll(selectors);
2108         if (!list.isEmpty()) {
2109             return (N) list.get(0);
2110         }
2111         return null;
2112     }
2113 
2114     /**
2115      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2116      *
2117      * Indicates if this node is currently attached to the page.
2118      * @return {@code true} if the page is one ancestor of the node.
2119      */
2120     public boolean isAttachedToPage() {
2121         return attachedToPage_;
2122     }
2123 
2124     /**
2125      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2126      *
2127      * Lifecycle method to support special processing for js method importNode.
2128      * @param doc the import target document
2129      * @see org.htmlunit.javascript.host.dom.Document#importNode(
2130      * org.htmlunit.javascript.host.dom.Node, boolean)
2131      * @see HtmlScript#processImportNode(org.htmlunit.javascript.host.dom.Document)
2132      */
2133     public void processImportNode(final org.htmlunit.javascript.host.dom.Document doc) {
2134         page_ = (SgmlPage) doc.getDomNodeOrDie();
2135     }
2136 
2137     /**
2138      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2139      *
2140      * Helper for a common call sequence.
2141      * @param feature the feature to check
2142      * @return {@code true} if the currently emulated browser has this feature.
2143      */
2144     public boolean hasFeature(final BrowserVersionFeatures feature) {
2145         return getPage().getWebClient().getBrowserVersion().hasFeature(feature);
2146     }
2147 
2148     private static final class CheckErrorHandler implements CSSErrorHandler {
2149         private CSSParseException error_;
2150 
2151         CSSParseException error() {
2152             return error_;
2153         }
2154 
2155         @Override
2156         public void warning(final CSSParseException exception) throws CSSException {
2157             // ignore
2158         }
2159 
2160         @Override
2161         public void fatalError(final CSSParseException exception) throws CSSException {
2162             error_ = exception;
2163         }
2164 
2165         @Override
2166         public void error(final CSSParseException exception) throws CSSException {
2167             error_ = exception;
2168         }
2169     }
2170 
2171     /**
2172      * Indicates if the provided event can be applied to this node.
2173      * Overwrite this.
2174      * @param event the event
2175      * @return {@code false} if the event can't be applied
2176      */
2177     public boolean handles(final Event event) {
2178         return true;
2179     }
2180 
2181     /**
2182      * Returns the previous sibling element node of this element.
2183      * null if this element has no element sibling nodes that come before this one in the document tree.
2184      * @return the previous sibling element node of this element.
2185      *         null if this element has no element sibling nodes that come before this one in the document tree
2186      */
2187     public DomElement getPreviousElementSibling() {
2188         DomNode node = getPreviousSibling();
2189         while (node != null && !(node instanceof DomElement)) {
2190             node = node.getPreviousSibling();
2191         }
2192         return (DomElement) node;
2193     }
2194 
2195     /**
2196      * Returns the next sibling element node of this element.
2197      * null if this element has no element sibling nodes that come after this one in the document tree.
2198      * @return the next sibling element node of this element.
2199      *         null if this element has no element sibling nodes that come after this one in the document tree
2200      */
2201     public DomElement getNextElementSibling() {
2202         DomNode node = getNextSibling();
2203         while (node != null && !(node instanceof DomElement)) {
2204             node = node.getNextSibling();
2205         }
2206         return (DomElement) node;
2207     }
2208 
2209     /**
2210      * @param selectorString the selector to test
2211      * @return the selected {@link DomElement} or null.
2212      */
2213     public DomElement closest(final String selectorString) {
2214         try {
2215             final WebClient webClient = getPage().getWebClient();
2216             final SelectorList selectorList = getSelectorList(selectorString, webClient);
2217 
2218             DomNode current = this;
2219             if (selectorList != null) {
2220                 do {
2221                     for (final Selector selector : selectorList) {
2222                         final DomElement elem = (DomElement) current;
2223                         if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, elem, null, true, true)) {
2224                             return elem;
2225                         }
2226                     }
2227 
2228                     do {
2229                         current = current.getParentNode();
2230                     }
2231                     while (current != null && !(current instanceof DomElement));
2232                 }
2233                 while (current != null);
2234             }
2235             return null;
2236         }
2237         catch (final IOException e) {
2238             throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage(), e);
2239         }
2240     }
2241 
2242     /**
2243      * An unmodifiable empty {@link NamedNodeMap} implementation.
2244      */
2245     private static final class ReadOnlyEmptyNamedNodeMapImpl implements NamedNodeMap, Serializable {
2246 
2247         /**
2248          * {@inheritDoc}
2249          */
2250         @Override
2251         public int getLength() {
2252             return 0;
2253         }
2254 
2255         /**
2256          * {@inheritDoc}
2257          */
2258         @Override
2259         public DomAttr getNamedItem(final String name) {
2260             return null;
2261         }
2262 
2263         /**
2264          * {@inheritDoc}
2265          */
2266         @Override
2267         public Node getNamedItemNS(final String namespaceURI, final String localName) {
2268             return null;
2269         }
2270 
2271         /**
2272          * {@inheritDoc}
2273          */
2274         @Override
2275         public Node item(final int index) {
2276             return null;
2277         }
2278 
2279         /**
2280          * {@inheritDoc}
2281          */
2282         @Override
2283         public Node removeNamedItem(final String name) throws DOMException {
2284             return null;
2285         }
2286 
2287         /**
2288          * {@inheritDoc}
2289          */
2290         @Override
2291         public Node removeNamedItemNS(final String namespaceURI, final String localName) {
2292             return null;
2293         }
2294 
2295         /**
2296          * {@inheritDoc}
2297          */
2298         @Override
2299         public DomAttr setNamedItem(final Node node) {
2300             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItem");
2301         }
2302 
2303         /**
2304          * {@inheritDoc}
2305          */
2306         @Override
2307         public Node setNamedItemNS(final Node node) throws DOMException {
2308             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItemNS");
2309         }
2310     }
2311 }