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