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             if (charsetName != null && this instanceof HtmlHtml) {
793                 printWriter.print("<?xml version=\"1.0\" encoding=\"");
794                 printWriter.print(charsetName);
795                 printWriter.print("\"?>\r\n");
796             }
797             printXml("", printWriter);
798             return stringWriter.toString();
799         }
800     }
801 
802     /**
803      * Recursively writes the XML data for the node tree starting at <code>node</code>.
804      *
805      * @param indent white space to indent child nodes
806      * @param printWriter writer where child nodes are written
807      */
808     protected void printXml(final String indent, final PrintWriter printWriter) {
809         printWriter.print(indent);
810         printWriter.print(this);
811         printWriter.print("\r\n");
812         printChildrenAsXml(indent, printWriter);
813     }
814 
815     /**
816      * Recursively writes the XML data for the node tree starting at <code>node</code>.
817      *
818      * @param indent white space to indent child nodes
819      * @param printWriter writer where child nodes are written
820      */
821     protected void printChildrenAsXml(final String indent, final PrintWriter printWriter) {
822         DomNode child = getFirstChild();
823         while (child != null) {
824             child.printXml(indent + "  ", printWriter);
825             child = child.getNextSibling();
826         }
827     }
828 
829     /**
830      * {@inheritDoc}
831      */
832     @Override
833     public String getNodeValue() {
834         return null;
835     }
836 
837     /**
838      * {@inheritDoc}
839      */
840     @Override
841     public DomNode cloneNode(final boolean deep) {
842         final DomNode newnode;
843         try {
844             newnode = (DomNode) clone();
845         }
846         catch (final CloneNotSupportedException e) {
847             throw new IllegalStateException("Clone not supported for node [" + this + "]", e);
848         }
849 
850         newnode.parent_ = null;
851         newnode.nextSibling_ = null;
852         newnode.previousSibling_ = null;
853         newnode.scriptObject_ = null;
854         newnode.firstChild_ = null;
855         newnode.attachedToPage_ = false;
856 
857         // if deep, clone the children too.
858         if (deep) {
859             for (DomNode child = firstChild_; child != null; child = child.nextSibling_) {
860                 newnode.appendChild(child.cloneNode(true));
861             }
862         }
863 
864         return newnode;
865     }
866 
867     /**
868      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
869      *
870      * <p>Returns the JavaScript object that corresponds to this node, lazily initializing a new one if necessary.</p>
871      *
872      * <p>The logic of when and where the JavaScript object is created needs a clean up: functions using
873      * a DOM node's JavaScript object should not have to check if they should create it first.</p>
874      *
875      * @param <T> the object type
876      * @return the JavaScript object that corresponds to this node
877      */
878     @SuppressWarnings("unchecked")
879     public <T extends HtmlUnitScriptable> T getScriptableObject() {
880         if (scriptObject_ == null) {
881             final SgmlPage page = getPage();
882             if (this == page) {
883                 final StringBuilder msg = new StringBuilder("No script object associated with the Page.");
884                 // because this is a strange case we like to provide as much info as possible
885                 msg.append(" class: '")
886                     .append(page.getClass().getName())
887                     .append('\'');
888                 try {
889                     msg.append(" url: '")
890                         .append(page.getUrl()).append("' content: ")
891                         .append(page.getWebResponse().getContentAsString());
892                 }
893                 catch (final Exception e) {
894                     // ok bad luck with detail
895                     msg.append(" no details: '").append(e).append('\'');
896                 }
897                 throw new IllegalStateException(msg.toString());
898             }
899             scriptObject_ = page.getScriptableObject().makeScriptableFor(this);
900         }
901         return (T) scriptObject_;
902     }
903 
904     /**
905      * {@inheritDoc}
906      */
907     @Override
908     public DomNode appendChild(final Node node) {
909         if (node == this) {
910             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add not to itself " + this);
911         }
912         final DomNode domNode = (DomNode) node;
913         if (domNode.isAncestorOf(this)) {
914             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Can not add (grand)parent to itself " + this);
915         }
916 
917         if (domNode instanceof DomDocumentFragment) {
918             final DomDocumentFragment fragment = (DomDocumentFragment) domNode;
919             for (final DomNode child : fragment.getChildren()) {
920                 appendChild(child);
921             }
922         }
923         else {
924             // clean up the new node, in case it is being moved
925             if (domNode.getParentNode() != null) {
926                 domNode.detach();
927             }
928 
929             basicAppend(domNode);
930 
931             fireAddition(domNode);
932         }
933 
934         return domNode;
935     }
936 
937     /**
938      * Appends the specified node to the end of this node's children, assuming the specified
939      * node is clean (doesn't have preexisting relationships to other nodes).
940      *
941      * @param node the node to append to this node's children
942      */
943     private void basicAppend(final DomNode node) {
944         // try to make the node setup as complete as possible
945         // before the node is reachable
946         node.setPage(getPage());
947         node.parent_ = this;
948 
949         if (firstChild_ == null) {
950             firstChild_ = node;
951         }
952         else {
953             final DomNode last = getLastChild();
954             node.previousSibling_ = last;
955             node.nextSibling_ = null; // safety first
956 
957             last.nextSibling_ = node;
958         }
959         firstChild_.previousSibling_ = node;
960     }
961 
962     /**
963      * {@inheritDoc}
964      */
965     @Override
966     public Node insertBefore(final Node newChild, final Node refChild) {
967         if (newChild instanceof DomDocumentFragment) {
968             final DomDocumentFragment fragment = (DomDocumentFragment) newChild;
969             for (final DomNode child : fragment.getChildren()) {
970                 insertBefore(child, refChild);
971             }
972             return newChild;
973         }
974 
975         if (refChild == null) {
976             appendChild(newChild);
977             return newChild;
978         }
979 
980         if (refChild.getParentNode() != this) {
981             throw new DOMException(DOMException.NOT_FOUND_ERR, "Reference node is not a child of this node.");
982         }
983 
984         ((DomNode) refChild).insertBefore((DomNode) newChild);
985         return newChild;
986     }
987 
988     /**
989      * Inserts the specified node as a new child node before this node into the child relationship this node is a
990      * part of. If the specified node is this node, this method is a no-op.
991      *
992      * @param newNode the new node to insert
993      */
994     public void insertBefore(final DomNode newNode) {
995         if (previousSibling_ == null) {
996             throw new IllegalStateException("Previous sibling for " + this + " is null.");
997         }
998 
999         if (newNode == this) {
1000             return;
1001         }
1002 
1003         // clean up the new node, in case it is being moved
1004         if (newNode.getParentNode() != null) {
1005             newNode.detach();
1006         }
1007 
1008         basicInsertBefore(newNode);
1009 
1010         fireAddition(newNode);
1011     }
1012 
1013     /**
1014      * Inserts the specified node into this node's parent's children right before this node, assuming the specified
1015      * node is clean (doesn't have preexisting relationships to other nodes).
1016      *
1017      * @param node the node to insert before this node
1018      */
1019     private void basicInsertBefore(final DomNode node) {
1020         // try to make the node setup as complete as possible
1021         // before the node is reachable
1022         node.setPage(page_);
1023         node.parent_ = parent_;
1024         node.previousSibling_ = previousSibling_;
1025         node.nextSibling_ = this;
1026 
1027         if (parent_.firstChild_ == this) {
1028             parent_.firstChild_ = node;
1029         }
1030         else {
1031             previousSibling_.nextSibling_ = node;
1032         }
1033         previousSibling_ = node;
1034     }
1035 
1036     private void fireAddition(final DomNode domNode) {
1037         final boolean wasAlreadyAttached = domNode.isAttachedToPage();
1038         domNode.attachedToPage_ = isAttachedToPage();
1039 
1040         final SgmlPage page = getPage();
1041         if (domNode.attachedToPage_) {
1042             // trigger events
1043             if (null != page && page.isHtmlPage()) {
1044                 ((HtmlPage) page).notifyNodeAdded(domNode);
1045             }
1046 
1047             // a node that is already "complete" (ie not being parsed) and not yet attached
1048             if (!domNode.isBodyParsed() && !wasAlreadyAttached) {
1049                 if (domNode.getFirstChild() != null) {
1050                     for (final Iterator<DomNode> iterator =
1051                             domNode.new DescendantDomNodesIterator(); iterator.hasNext();) {
1052                         final DomNode child = iterator.next();
1053                         child.attachedToPage_ = true;
1054                         child.onAllChildrenAddedToPage(true);
1055                     }
1056                 }
1057                 domNode.onAllChildrenAddedToPage(true);
1058             }
1059         }
1060 
1061         if (this instanceof DomDocumentFragment) {
1062             onAddedToDocumentFragment();
1063         }
1064 
1065         if (page == null || page.isDomChangeListenerInUse()) {
1066             fireNodeAdded(this, domNode);
1067         }
1068     }
1069 
1070     /**
1071      * Indicates if the current node is being parsed. This means that the opening tag has already been
1072      * parsed but not the body and end tag.
1073      */
1074     private boolean isBodyParsed() {
1075         return getStartLineNumber() != -1 && getEndLineNumber() == -1;
1076     }
1077 
1078     /**
1079      * Recursively sets the new page on the node and its children
1080      * @param newPage the new owning page
1081      */
1082     private void setPage(final SgmlPage newPage) {
1083         if (page_ == newPage) {
1084             return; // nothing to do
1085         }
1086 
1087         page_ = newPage;
1088         for (final DomNode node : getChildren()) {
1089             node.setPage(newPage);
1090         }
1091     }
1092 
1093     /**
1094      * {@inheritDoc}
1095      */
1096     @Override
1097     public Node removeChild(final Node child) {
1098         if (child.getParentNode() != this) {
1099             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1100         }
1101         ((DomNode) child).remove();
1102         return child;
1103     }
1104 
1105     /**
1106      * Removes all of this node's children.
1107      */
1108     public void removeAllChildren() {
1109         while (getFirstChild() != null) {
1110             getFirstChild().remove();
1111         }
1112     }
1113 
1114     /**
1115      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1116      *
1117      * Parses the specified HTML source code, appending the resulting content at the specified target location.
1118      * @param source the HTML code extract to parse
1119      * @throws IOException in case of error
1120      * @throws SAXException in case of error
1121      */
1122     public void parseHtmlSnippet(final String source) throws SAXException, IOException {
1123         getPage().getWebClient().getPageCreator().getHtmlParser().parseFragment(this, source);
1124     }
1125 
1126     /**
1127      * Removes this node from all relationships with other nodes.
1128      */
1129     public void remove() {
1130         // same as detach for the moment
1131         detach();
1132     }
1133 
1134     /**
1135      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1136      *
1137      * Detach this node from all relationships with other nodes.
1138      * This is the first step of a move.
1139      */
1140     protected void detach() {
1141         final DomNode exParent = parent_;
1142 
1143         basicRemove();
1144 
1145         fireRemoval(exParent);
1146     }
1147 
1148     /**
1149      * Cuts off all relationships this node has with siblings and parents.
1150      */
1151     protected void basicRemove() {
1152         if (parent_ != null && parent_.firstChild_ == this) {
1153             parent_.firstChild_ = nextSibling_;
1154         }
1155         else if (previousSibling_ != null && previousSibling_.nextSibling_ == this) {
1156             previousSibling_.nextSibling_ = nextSibling_;
1157         }
1158         if (nextSibling_ != null && nextSibling_.previousSibling_ == this) {
1159             nextSibling_.previousSibling_ = previousSibling_;
1160         }
1161         if (parent_ != null && this == parent_.getLastChild()) {
1162             parent_.firstChild_.previousSibling_ = previousSibling_;
1163         }
1164 
1165         nextSibling_ = null;
1166         previousSibling_ = null;
1167         parent_ = null;
1168         attachedToPage_ = false;
1169         for (final DomNode descendant : getDescendants()) {
1170             descendant.attachedToPage_ = false;
1171         }
1172     }
1173 
1174     private void fireRemoval(final DomNode exParent) {
1175         final SgmlPage page = getPage();
1176         if (page != null && page instanceof HtmlPage) {
1177             // some actions executed on removal need an intact parent relationship (e.g. for the
1178             // DocumentPositionComparator) so we have to restore it temporarily
1179             parent_ = exParent;
1180             ((HtmlPage) page).notifyNodeRemoved(this);
1181             parent_ = null;
1182         }
1183 
1184         if (exParent != null && (page == null || page.isDomChangeListenerInUse())) {
1185             fireNodeDeleted(exParent, this);
1186             // ask ex-parent to fire event (because we don't have parent now)
1187             exParent.fireNodeDeleted(exParent, this);
1188         }
1189     }
1190 
1191     /**
1192      * {@inheritDoc}
1193      */
1194     @Override
1195     public Node replaceChild(final Node newChild, final Node oldChild) {
1196         if (oldChild.getParentNode() != this) {
1197             throw new DOMException(DOMException.NOT_FOUND_ERR, "Node is not a child of this node.");
1198         }
1199         ((DomNode) oldChild).replace((DomNode) newChild);
1200         return oldChild;
1201     }
1202 
1203     /**
1204      * Replaces this node with another node. If the specified node is this node, this
1205      * method is a no-op.
1206      * @param newNode the node to replace this one
1207      */
1208     public void replace(final DomNode newNode) {
1209         if (newNode != this) {
1210             final DomNode exParent = parent_;
1211             final DomNode exNextSibling = nextSibling_;
1212 
1213             remove();
1214 
1215             exParent.insertBefore(newNode, exNextSibling);
1216         }
1217     }
1218 
1219     /**
1220      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1221      *
1222      * Quietly removes this node and moves its children to the specified destination. "Quietly" means
1223      * that no node events are fired. This method is not appropriate for most use cases. It should
1224      * only be used in specific cases for HTML parsing hackery.
1225      *
1226      * @param destination the node to which this node's children should be moved before this node is removed
1227      */
1228     public void quietlyRemoveAndMoveChildrenTo(final DomNode destination) {
1229         if (destination.getPage() != getPage()) {
1230             throw new RuntimeException("Cannot perform quiet move on nodes from different pages.");
1231         }
1232         for (final DomNode child : getChildren()) {
1233             if (child != destination) {
1234                 child.basicRemove();
1235                 destination.basicAppend(child);
1236             }
1237         }
1238         basicRemove();
1239     }
1240 
1241     /**
1242      * Check for insertion errors for a new child node. This is overridden by derived
1243      * classes to enforce which types of children are allowed.
1244      *
1245      * @param newChild the new child node that is being inserted below this node
1246      * @throws DOMException HIERARCHY_REQUEST_ERR: Raised if this node is of a type that does
1247      *         not allow children of the type of the newChild node, or if the node to insert is one of
1248      *         this node's ancestors or this node itself, or if this node is of type Document and the
1249      *         DOM application attempts to insert a second DocumentType or Element node.
1250      *         WRONG_DOCUMENT_ERR: Raised if newChild was created from a different document than the
1251      *         one that created this node.
1252      */
1253     protected void checkChildHierarchy(final Node newChild) throws DOMException {
1254         Node parentNode = this;
1255         while (parentNode != null) {
1256             if (parentNode == newChild) {
1257                 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Child node is already a parent.");
1258             }
1259             parentNode = parentNode.getParentNode();
1260         }
1261         final Document thisDocument = getOwnerDocument();
1262         final Document childDocument = newChild.getOwnerDocument();
1263         if (childDocument != thisDocument && childDocument != null) {
1264             throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Child node " + newChild.getNodeName()
1265                 + " is not in the same Document as this " + getNodeName() + ".");
1266         }
1267     }
1268 
1269     /**
1270      * Lifecycle method invoked whenever a node is added to a page. Intended to
1271      * be overridden by nodes which need to perform custom logic when they are
1272      * added to a page. This method is recursive, so if you override it, please
1273      * be sure to call <code>super.onAddedToPage()</code>.
1274      */
1275     protected void onAddedToPage() {
1276         if (firstChild_ != null) {
1277             for (final DomNode child : getChildren()) {
1278                 child.onAddedToPage();
1279             }
1280         }
1281     }
1282 
1283     /**
1284      * Lifecycle method invoked after a node and all its children have been added to a page, during
1285      * parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic
1286      * after they and all their child nodes have been processed by the HTML parser. This method is
1287      * not recursive, and the default implementation is empty, so there is no need to call
1288      * <code>super.onAllChildrenAddedToPage()</code> if you implement this method.
1289      * @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no
1290      */
1291     public void onAllChildrenAddedToPage(final boolean postponed) {
1292         // Empty by default.
1293     }
1294 
1295     /**
1296      * Lifecycle method invoked whenever a node is added to a document fragment. Intended to
1297      * be overridden by nodes which need to perform custom logic when they are
1298      * added to a fragment. This method is recursive, so if you override it, please
1299      * be sure to call <code>super.onAddedToDocumentFragment()</code>.
1300      */
1301     protected void onAddedToDocumentFragment() {
1302         if (firstChild_ != null) {
1303             for (final DomNode child : getChildren()) {
1304                 child.onAddedToDocumentFragment();
1305             }
1306         }
1307     }
1308 
1309     /**
1310      * @return an {@link Iterable} over the children of this node
1311      */
1312     public final Iterable<DomNode> getChildren() {
1313         return () -> new ChildIterator(firstChild_);
1314     }
1315 
1316     /**
1317      * An iterator over all children of this node.
1318      */
1319     protected static class ChildIterator implements Iterator<DomNode> {
1320 
1321         private DomNode nextNode_;
1322         private DomNode currentNode_;
1323 
1324         public ChildIterator(final DomNode nextNode) {
1325             nextNode_ = nextNode;
1326         }
1327 
1328         /** {@inheritDoc} */
1329         @Override
1330         public boolean hasNext() {
1331             return nextNode_ != null;
1332         }
1333 
1334         /** {@inheritDoc} */
1335         @Override
1336         public DomNode next() {
1337             if (nextNode_ != null) {
1338                 currentNode_ = nextNode_;
1339                 nextNode_ = nextNode_.nextSibling_;
1340                 return currentNode_;
1341             }
1342             throw new NoSuchElementException();
1343         }
1344 
1345         /** {@inheritDoc} */
1346         @Override
1347         public void remove() {
1348             if (currentNode_ == null) {
1349                 throw new IllegalStateException();
1350             }
1351             currentNode_.remove();
1352         }
1353     }
1354 
1355     /**
1356      * Returns an {@link Iterable} that will recursively iterate over all of this node's descendants,
1357      * including {@link DomText} elements, {@link DomComment} elements, etc. If you want to iterate
1358      * only over {@link HtmlElement} descendants, please use {@link #getHtmlElementDescendants()}.
1359      * @return an {@link Iterable} that will recursively iterate over all of this node's descendants
1360      */
1361     public final Iterable<DomNode> getDescendants() {
1362         return () -> new DescendantDomNodesIterator();
1363     }
1364 
1365     /**
1366      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1367      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1368      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1369      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link HtmlElement}
1370      *         descendants
1371      * @see #getDomElementDescendants()
1372      */
1373     public final Iterable<HtmlElement> getHtmlElementDescendants() {
1374         return () -> new DescendantHtmlElementsIterator();
1375     }
1376 
1377     /**
1378      * Returns an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1379      * descendants. If you want to iterate over all descendants (including {@link DomText} elements,
1380      * {@link DomComment} elements, etc.), please use {@link #getDescendants()}.
1381      * @return an {@link Iterable} that will recursively iterate over all of this node's {@link DomElement}
1382      *         descendants
1383      * @see #getHtmlElementDescendants()
1384      */
1385     public final Iterable<DomElement> getDomElementDescendants() {
1386         return () -> new DescendantDomElementsIterator();
1387     }
1388 
1389     /**
1390      * Iterates over all descendants of a specific type, in document order.
1391      * @param <T> the type of nodes over which to iterate
1392      *
1393      * @deprecated as of version 4.7.0; use {@link DescendantDomNodesIterator},
1394      *     {@link DescendantDomElementsIterator}, or {@link DescendantHtmlElementsIterator} instead.
1395      */
1396     @Deprecated
1397     protected class DescendantElementsIterator<T extends DomNode> implements Iterator<T> {
1398 
1399         private DomNode currentNode_;
1400         private DomNode nextNode_;
1401         private final Class<T> type_;
1402 
1403         /**
1404          * Creates a new instance which iterates over the specified node type.
1405          * @param type the type of nodes over which to iterate
1406          */
1407         public DescendantElementsIterator(final Class<T> type) {
1408             type_ = type;
1409             nextNode_ = getFirstChildElement(DomNode.this);
1410         }
1411 
1412         /** {@inheritDoc} */
1413         @Override
1414         public boolean hasNext() {
1415             return nextNode_ != null;
1416         }
1417 
1418         /** {@inheritDoc} */
1419         @Override
1420         public T next() {
1421             return nextNode();
1422         }
1423 
1424         /** {@inheritDoc} */
1425         @Override
1426         public void remove() {
1427             if (currentNode_ == null) {
1428                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1429             }
1430             final DomNode current = currentNode_;
1431             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1432                 next();
1433             }
1434             current.remove();
1435         }
1436 
1437         /**
1438          * @return the next node, if there is one
1439          */
1440         @SuppressWarnings("unchecked")
1441         public T nextNode() {
1442             currentNode_ = nextNode_;
1443             setNextElement();
1444             return (T) currentNode_;
1445         }
1446 
1447         private void setNextElement() {
1448             DomNode next = getFirstChildElement(nextNode_);
1449             if (next == null) {
1450                 next = getNextDomSibling(nextNode_);
1451             }
1452             if (next == null) {
1453                 next = getNextElementUpwards(nextNode_);
1454             }
1455             nextNode_ = next;
1456         }
1457 
1458         private DomNode getNextElementUpwards(final DomNode startingNode) {
1459             if (startingNode == DomNode.this) {
1460                 return null;
1461             }
1462 
1463             DomNode parent = startingNode.getParentNode();
1464             while (parent != null && parent != DomNode.this) {
1465                 DomNode next = parent.getNextSibling();
1466                 while (next != null && !isAccepted(next)) {
1467                     next = next.getNextSibling();
1468                 }
1469                 if (next != null) {
1470                     return next;
1471                 }
1472                 parent = parent.getParentNode();
1473             }
1474             return null;
1475         }
1476 
1477         private DomNode getFirstChildElement(final DomNode parent) {
1478             DomNode node = parent.getFirstChild();
1479             while (node != null && !isAccepted(node)) {
1480                 node = node.getNextSibling();
1481             }
1482             return node;
1483         }
1484 
1485         /**
1486          * Indicates if the node is accepted. If not it won't be explored at all.
1487          * @param node the node to test
1488          * @return {@code true} if accepted
1489          */
1490         protected boolean isAccepted(final DomNode node) {
1491             return type_.isAssignableFrom(node.getClass());
1492         }
1493 
1494         private DomNode getNextDomSibling(final DomNode element) {
1495             DomNode node = element.getNextSibling();
1496             while (node != null && !isAccepted(node)) {
1497                 node = node.getNextSibling();
1498             }
1499             return node;
1500         }
1501     }
1502 
1503     /**
1504      * Iterates over all descendants DomNodes, in document order.
1505      */
1506     protected final class DescendantDomNodesIterator implements Iterator<DomNode> {
1507         private DomNode currentNode_;
1508         private DomNode nextNode_;
1509 
1510         /**
1511          * Creates a new instance which iterates over the specified node type.
1512          */
1513         public DescendantDomNodesIterator() {
1514             nextNode_ = getFirstChildElement(DomNode.this);
1515         }
1516 
1517         /** {@inheritDoc} */
1518         @Override
1519         public boolean hasNext() {
1520             return nextNode_ != null;
1521         }
1522 
1523         /** {@inheritDoc} */
1524         @Override
1525         public DomNode next() {
1526             return nextNode();
1527         }
1528 
1529         /** {@inheritDoc} */
1530         @Override
1531         public void remove() {
1532             if (currentNode_ == null) {
1533                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1534             }
1535             final DomNode current = currentNode_;
1536             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1537                 next();
1538             }
1539             current.remove();
1540         }
1541 
1542         /**
1543          * @return the next node, if there is one
1544          */
1545         @SuppressWarnings("unchecked")
1546         public DomNode nextNode() {
1547             currentNode_ = nextNode_;
1548 
1549             DomNode next = getFirstChildElement(nextNode_);
1550             if (next == null) {
1551                 next = getNextDomSibling(nextNode_);
1552             }
1553             if (next == null) {
1554                 next = getNextElementUpwards(nextNode_);
1555             }
1556             nextNode_ = next;
1557 
1558             return currentNode_;
1559         }
1560 
1561         private DomNode getNextElementUpwards(final DomNode startingNode) {
1562             if (startingNode == DomNode.this) {
1563                 return null;
1564             }
1565 
1566             DomNode parent = startingNode.getParentNode();
1567             while (parent != null && parent != DomNode.this) {
1568                 DomNode next = parent.getNextSibling();
1569                 while (next != null && !isAccepted(next)) {
1570                     next = next.getNextSibling();
1571                 }
1572                 if (next != null) {
1573                     return next;
1574                 }
1575                 parent = parent.getParentNode();
1576             }
1577             return null;
1578         }
1579 
1580         private DomNode getFirstChildElement(final DomNode parent) {
1581             DomNode node = parent.getFirstChild();
1582             while (node != null && !isAccepted(node)) {
1583                 node = node.getNextSibling();
1584             }
1585             return node;
1586         }
1587 
1588         /**
1589          * Indicates if the node is accepted. If not it won't be explored at all.
1590          * @param node the node to test
1591          * @return {@code true} if accepted
1592          */
1593         private boolean isAccepted(final DomNode node) {
1594             return DomNode.class.isAssignableFrom(node.getClass());
1595         }
1596 
1597         private DomNode getNextDomSibling(final DomNode element) {
1598             DomNode node = element.getNextSibling();
1599             while (node != null && !isAccepted(node)) {
1600                 node = node.getNextSibling();
1601             }
1602             return node;
1603         }
1604     }
1605 
1606     /**
1607      * Iterates over all descendants DomTypes, in document order.
1608      */
1609     protected final class DescendantDomElementsIterator implements Iterator<DomElement> {
1610         private DomNode currentNode_;
1611         private DomNode nextNode_;
1612 
1613         /**
1614          * Creates a new instance which iterates over the specified node type.
1615          */
1616         public DescendantDomElementsIterator() {
1617             nextNode_ = getFirstChildElement(DomNode.this);
1618         }
1619 
1620         /** {@inheritDoc} */
1621         @Override
1622         public boolean hasNext() {
1623             return nextNode_ != null;
1624         }
1625 
1626         /** {@inheritDoc} */
1627         @Override
1628         public DomElement next() {
1629             return nextNode();
1630         }
1631 
1632         /** {@inheritDoc} */
1633         @Override
1634         public void remove() {
1635             if (currentNode_ == null) {
1636                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1637             }
1638             final DomNode current = currentNode_;
1639             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1640                 next();
1641             }
1642             current.remove();
1643         }
1644 
1645         /**
1646          * @return the next node, if there is one
1647          */
1648         @SuppressWarnings("unchecked")
1649         public DomElement nextNode() {
1650             currentNode_ = nextNode_;
1651 
1652             DomNode next = getFirstChildElement(nextNode_);
1653             if (next == null) {
1654                 next = getNextDomSibling(nextNode_);
1655             }
1656             if (next == null) {
1657                 next = getNextElementUpwards(nextNode_);
1658             }
1659             nextNode_ = next;
1660 
1661             return (DomElement) currentNode_;
1662         }
1663 
1664         private DomNode getNextElementUpwards(final DomNode startingNode) {
1665             if (startingNode == DomNode.this) {
1666                 return null;
1667             }
1668 
1669             DomNode parent = startingNode.getParentNode();
1670             while (parent != null && parent != DomNode.this) {
1671                 DomNode next = parent.getNextSibling();
1672                 while (next != null && !isAccepted(next)) {
1673                     next = next.getNextSibling();
1674                 }
1675                 if (next != null) {
1676                     return next;
1677                 }
1678                 parent = parent.getParentNode();
1679             }
1680             return null;
1681         }
1682 
1683         private DomNode getFirstChildElement(final DomNode parent) {
1684             DomNode node = parent.getFirstChild();
1685             while (node != null && !isAccepted(node)) {
1686                 node = node.getNextSibling();
1687             }
1688             return node;
1689         }
1690 
1691         /**
1692          * Indicates if the node is accepted. If not it won't be explored at all.
1693          * @param node the node to test
1694          * @return {@code true} if accepted
1695          */
1696         private boolean isAccepted(final DomNode node) {
1697             return DomElement.class.isAssignableFrom(node.getClass());
1698         }
1699 
1700         private DomNode getNextDomSibling(final DomNode element) {
1701             DomNode node = element.getNextSibling();
1702             while (node != null && !isAccepted(node)) {
1703                 node = node.getNextSibling();
1704             }
1705             return node;
1706         }
1707     }
1708 
1709     /**
1710      * Iterates over all descendants HtmlElements, in document order.
1711      */
1712     protected final class DescendantHtmlElementsIterator implements Iterator<HtmlElement> {
1713         private DomNode currentNode_;
1714         private DomNode nextNode_;
1715 
1716         /**
1717          * Creates a new instance which iterates over the specified node type.
1718          */
1719         public DescendantHtmlElementsIterator() {
1720             nextNode_ = getFirstChildElement(DomNode.this);
1721         }
1722 
1723         /** {@inheritDoc} */
1724         @Override
1725         public boolean hasNext() {
1726             return nextNode_ != null;
1727         }
1728 
1729         /** {@inheritDoc} */
1730         @Override
1731         public HtmlElement next() {
1732             return nextNode();
1733         }
1734 
1735         /** {@inheritDoc} */
1736         @Override
1737         public void remove() {
1738             if (currentNode_ == null) {
1739                 throw new IllegalStateException("Unable to remove current node, because there is no current node.");
1740             }
1741             final DomNode current = currentNode_;
1742             while (nextNode_ != null && current.isAncestorOf(nextNode_)) {
1743                 next();
1744             }
1745             current.remove();
1746         }
1747 
1748         /**
1749          * @return the next node, if there is one
1750          */
1751         @SuppressWarnings("unchecked")
1752         public HtmlElement nextNode() {
1753             currentNode_ = nextNode_;
1754 
1755             DomNode next = getFirstChildElement(nextNode_);
1756             if (next == null) {
1757                 next = getNextDomSibling(nextNode_);
1758             }
1759             if (next == null) {
1760                 next = getNextElementUpwards(nextNode_);
1761             }
1762             nextNode_ = next;
1763 
1764             return (HtmlElement) currentNode_;
1765         }
1766 
1767         private DomNode getNextElementUpwards(final DomNode startingNode) {
1768             if (startingNode == DomNode.this) {
1769                 return null;
1770             }
1771 
1772             DomNode parent = startingNode.getParentNode();
1773             while (parent != null && parent != DomNode.this) {
1774                 DomNode next = parent.getNextSibling();
1775                 while (next != null && !isAccepted(next)) {
1776                     next = next.getNextSibling();
1777                 }
1778                 if (next != null) {
1779                     return next;
1780                 }
1781                 parent = parent.getParentNode();
1782             }
1783             return null;
1784         }
1785 
1786         private DomNode getFirstChildElement(final DomNode parent) {
1787             DomNode node = parent.getFirstChild();
1788             while (node != null && !isAccepted(node)) {
1789                 node = node.getNextSibling();
1790             }
1791             return node;
1792         }
1793 
1794         /**
1795          * Indicates if the node is accepted. If not it won't be explored at all.
1796          * @param node the node to test
1797          * @return {@code true} if accepted
1798          */
1799         private boolean isAccepted(final DomNode node) {
1800             return HtmlElement.class.isAssignableFrom(node.getClass());
1801         }
1802 
1803         private DomNode getNextDomSibling(final DomNode element) {
1804             DomNode node = element.getNextSibling();
1805             while (node != null && !isAccepted(node)) {
1806                 node = node.getNextSibling();
1807             }
1808             return node;
1809         }
1810     }
1811 
1812     /**
1813      * Returns this node's ready state (IE only).
1814      * @return this node's ready state
1815      */
1816     public String getReadyState() {
1817         return readyState_;
1818     }
1819 
1820     /**
1821      * Sets this node's ready state (IE only).
1822      * @param state this node's ready state
1823      */
1824     public void setReadyState(final String state) {
1825         readyState_ = state;
1826     }
1827 
1828     /**
1829      * Evaluates the specified XPath expression from this node, returning the matching elements.
1830      * <br>
1831      * Note: This implies that the ',' point to this node but the general axis like '//' are still
1832      * looking at the whole document. E.g. if you like to get all child h1 nodes from the current one
1833      * you have to use './/h1' instead of '//h1' because the latter matches all h1 nodes of the#
1834      * whole document.
1835      *
1836      * @param <T> the expected type
1837      * @param xpathExpr the XPath expression to evaluate
1838      * @return the elements which match the specified XPath expression
1839      * @see #getFirstByXPath(String)
1840      * @see #getCanonicalXPath()
1841      */
1842     public <T> List<T> getByXPath(final String xpathExpr) {
1843         return XPathHelper.getByXPath(this, xpathExpr, null);
1844     }
1845 
1846     /**
1847      * Evaluates the specified XPath expression from this node, returning the matching elements.
1848      *
1849      * @param xpathExpr the XPath expression to evaluate
1850      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1851      * @return the elements which match the specified XPath expression
1852      * @see #getFirstByXPath(String)
1853      * @see #getCanonicalXPath()
1854      */
1855     public List<?> getByXPath(final String xpathExpr, final PrefixResolver resolver) {
1856         return XPathHelper.getByXPath(this, xpathExpr, resolver);
1857     }
1858 
1859     /**
1860      * Evaluates the specified XPath expression from this node, returning the first matching element,
1861      * or {@code null} if no node matches the specified XPath expression.
1862      *
1863      * @param xpathExpr the XPath expression
1864      * @param <X> the expression type
1865      * @return the first element matching the specified XPath expression
1866      * @see #getByXPath(String)
1867      * @see #getCanonicalXPath()
1868      */
1869     public <X> X getFirstByXPath(final String xpathExpr) {
1870         return getFirstByXPath(xpathExpr, null);
1871     }
1872 
1873     /**
1874      * Evaluates the specified XPath expression from this node, returning the first matching element,
1875      * or {@code null} if no node matches the specified XPath expression.
1876      *
1877      * @param xpathExpr the XPath expression
1878      * @param <X> the expression type
1879      * @param resolver the prefix resolver to use for resolving namespace prefixes, or null
1880      * @return the first element matching the specified XPath expression
1881      * @see #getByXPath(String)
1882      * @see #getCanonicalXPath()
1883      */
1884     @SuppressWarnings("unchecked")
1885     public <X> X getFirstByXPath(final String xpathExpr, final PrefixResolver resolver) {
1886         final List<?> results = getByXPath(xpathExpr, resolver);
1887         if (results.isEmpty()) {
1888             return null;
1889         }
1890         return (X) results.get(0);
1891     }
1892 
1893     /**
1894      * <p>Returns the canonical XPath expression which identifies this node, for instance
1895      * <code>"/html/body/table[3]/tbody/tr[5]/td[2]/span/a[3]"</code>.</p>
1896      *
1897      * <p><span style="color:red">WARNING:</span> This sort of automated XPath expression
1898      * is often quite bad at identifying a node, as it is highly sensitive to changes in
1899      * the DOM tree.</p>
1900      *
1901      * @return the canonical XPath expression which identifies this node
1902      * @see #getByXPath(String)
1903      */
1904     public String getCanonicalXPath() {
1905         throw new RuntimeException("Method getCanonicalXPath() not implemented for nodes of type " + getNodeType());
1906     }
1907 
1908     /**
1909      * Notifies the registered {@link IncorrectnessListener} of something that is not fully correct.
1910      * @param message the notification to send to the registered {@link IncorrectnessListener}
1911      */
1912     protected void notifyIncorrectness(final String message) {
1913         final WebClient client = getPage().getEnclosingWindow().getWebClient();
1914         final IncorrectnessListener incorrectnessListener = client.getIncorrectnessListener();
1915         incorrectnessListener.notify(message, this);
1916     }
1917 
1918     /**
1919      * Adds a {@link DomChangeListener} to the listener list. The listener is registered for
1920      * all descendants of this node.
1921      *
1922      * @param listener the DOM structure change listener to be added
1923      * @see #removeDomChangeListener(DomChangeListener)
1924      */
1925     public void addDomChangeListener(final DomChangeListener listener) {
1926         WebAssert.notNull("listener", listener);
1927 
1928         synchronized (this) {
1929             if (domListeners_ == null) {
1930                 domListeners_ = new ArrayList<>();
1931             }
1932             domListeners_.add(listener);
1933 
1934             final SgmlPage page = getPage();
1935             if (page != null) {
1936                 page.domChangeListenerAdded();
1937             }
1938         }
1939     }
1940 
1941     /**
1942      * Removes a {@link DomChangeListener} from the listener list. The listener is deregistered for
1943      * all descendants of this node.
1944      *
1945      * @param listener the DOM structure change listener to be removed
1946      * @see #addDomChangeListener(DomChangeListener)
1947      */
1948     public void removeDomChangeListener(final DomChangeListener listener) {
1949         WebAssert.notNull("listener", listener);
1950 
1951         synchronized (this) {
1952             if (domListeners_ != null) {
1953                 domListeners_.remove(listener);
1954             }
1955         }
1956     }
1957 
1958     /**
1959      * Support for reporting DOM changes. This method can be called when a node has been added, and it
1960      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
1961      *
1962      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeAdded(DomNode, DomNode)}.</p>
1963      *
1964      * @param parentNode the parent of the node that was changed
1965      * @param addedNode the node that has been added
1966      */
1967     protected void fireNodeAdded(final DomNode parentNode, final DomNode addedNode) {
1968         DomChangeEvent event = null;
1969 
1970         DomNode toInform = this;
1971         while (toInform != null) {
1972             if (toInform.domListeners_ != null) {
1973                 final List<DomChangeListener> listeners;
1974                 synchronized (toInform) {
1975                     listeners = new ArrayList<>(toInform.domListeners_);
1976                 }
1977 
1978                 if (event == null) {
1979                     event = new DomChangeEvent(parentNode, addedNode);
1980                 }
1981                 for (final DomChangeListener domChangeListener : listeners) {
1982                     domChangeListener.nodeAdded(event);
1983                 }
1984             }
1985 
1986             toInform = toInform.getParentNode();
1987         }
1988     }
1989 
1990     /**
1991      * Adds a {@link CharacterDataChangeListener} to the listener list. The listener is registered for
1992      * all descendants of this node.
1993      *
1994      * @param listener the character data change listener to be added
1995      * @see #removeCharacterDataChangeListener(CharacterDataChangeListener)
1996      */
1997     public void addCharacterDataChangeListener(final CharacterDataChangeListener listener) {
1998         WebAssert.notNull("listener", listener);
1999 
2000         synchronized (this) {
2001             if (characterDataListeners_ == null) {
2002                 characterDataListeners_ = new ArrayList<>();
2003             }
2004             characterDataListeners_.add(listener);
2005 
2006             final SgmlPage page = getPage();
2007             if (page != null) {
2008                 page.characterDataChangeListenerAdded();
2009             }
2010         }
2011     }
2012 
2013     /**
2014      * Removes a {@link CharacterDataChangeListener} from the listener list. The listener is deregistered for
2015      * all descendants of this node.
2016      *
2017      * @param listener the Character Data change listener to be removed
2018      * @see #addCharacterDataChangeListener(CharacterDataChangeListener)
2019      */
2020     public void removeCharacterDataChangeListener(final CharacterDataChangeListener listener) {
2021         WebAssert.notNull("listener", listener);
2022 
2023         synchronized (this) {
2024             if (characterDataListeners_ != null) {
2025                 characterDataListeners_.remove(listener);
2026             }
2027         }
2028     }
2029 
2030     /**
2031      * Support for reporting Character Data changes.
2032      *
2033      * <p>Note that this method recursively calls this node's parent's {@link #fireCharacterDataChanged}.</p>
2034      *
2035      * @param characterData the character data which is changed
2036      * @param oldValue the old value
2037      */
2038     protected void fireCharacterDataChanged(final DomCharacterData characterData, final String oldValue) {
2039         CharacterDataChangeEvent event = null;
2040 
2041         DomNode toInform = this;
2042         while (toInform != null) {
2043             if (toInform.characterDataListeners_ != null) {
2044                 final List<CharacterDataChangeListener> listeners;
2045                 synchronized (toInform) {
2046                     listeners = new ArrayList<>(toInform.characterDataListeners_);
2047                 }
2048 
2049                 if (event == null) {
2050                     event = new CharacterDataChangeEvent(characterData, oldValue);
2051                 }
2052                 for (final CharacterDataChangeListener domChangeListener : listeners) {
2053                     domChangeListener.characterDataChanged(event);
2054                 }
2055             }
2056 
2057             toInform = toInform.getParentNode();
2058         }
2059     }
2060 
2061     /**
2062      * Support for reporting DOM changes. This method can be called when a node has been deleted, and it
2063      * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
2064      *
2065      * <p>Note that this method recursively calls this node's parent's {@link #fireNodeDeleted(DomNode, DomNode)}.</p>
2066      *
2067      * @param parentNode the parent of the node that was changed
2068      * @param deletedNode the node that has been deleted
2069      */
2070     protected void fireNodeDeleted(final DomNode parentNode, final DomNode deletedNode) {
2071         DomChangeEvent event = null;
2072 
2073         DomNode toInform = this;
2074         while (toInform != null) {
2075             if (toInform.domListeners_ != null) {
2076                 final List<DomChangeListener> listeners;
2077                 synchronized (toInform) {
2078                     listeners = new ArrayList<>(toInform.domListeners_);
2079                 }
2080 
2081                 if (event == null) {
2082                     event = new DomChangeEvent(parentNode, deletedNode);
2083                 }
2084                 for (final DomChangeListener domChangeListener : listeners) {
2085                     domChangeListener.nodeDeleted(event);
2086                 }
2087             }
2088 
2089             toInform = toInform.getParentNode();
2090         }
2091     }
2092 
2093     /**
2094      * Retrieves all element nodes from descendants of the starting element node that match any selector
2095      * within the supplied selector strings.
2096      * @param selectors one or more CSS selectors separated by commas
2097      * @return list of all found nodes
2098      */
2099     public DomNodeList<DomNode> querySelectorAll(final String selectors) {
2100         try {
2101             final WebClient webClient = getPage().getWebClient();
2102             final SelectorList selectorList = getSelectorList(selectors, webClient);
2103 
2104             final List<DomNode> elements = new ArrayList<>();
2105             if (selectorList != null) {
2106                 for (final DomElement child : getDomElementDescendants()) {
2107                     for (final Selector selector : selectorList) {
2108                         if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, child, null, true, true)) {
2109                             elements.add(child);
2110                             break;
2111                         }
2112                     }
2113                 }
2114             }
2115             return new StaticDomNodeList(elements);
2116         }
2117         catch (final IOException e) {
2118             throw new CSSException("Error parsing CSS selectors from '" + selectors + "': " + e.getMessage(), e);
2119         }
2120     }
2121 
2122     /**
2123      * Returns the {@link SelectorList}.
2124      * @param selectors the selectors
2125      * @param webClient the {@link WebClient}
2126      * @return the {@link SelectorList}
2127      * @throws IOException if an error occurs
2128      */
2129     protected SelectorList getSelectorList(final String selectors, final WebClient webClient)
2130             throws IOException {
2131 
2132         // get us a CSS3Parser from the pool so the chance of reusing it are high
2133         try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
2134             final CSSOMParser parser = new CSSOMParser(pooledParser);
2135             final CheckErrorHandler errorHandler = new CheckErrorHandler();
2136             parser.setErrorHandler(errorHandler);
2137 
2138             final SelectorList selectorList = parser.parseSelectors(selectors);
2139             // in case of error parseSelectors returns null
2140             if (errorHandler.error() != null) {
2141                 throw new CSSException("Invalid selectors: '" + selectors + "'", errorHandler.error());
2142             }
2143 
2144             if (selectorList != null) {
2145                 CssStyleSheet.validateSelectors(selectorList, this);
2146 
2147             }
2148             return selectorList;
2149         }
2150     }
2151 
2152     /**
2153      * Returns the first element within the document that matches the specified group of selectors.
2154      * @param selectors one or more CSS selectors separated by commas
2155      * @param <N> the node type
2156      * @return null if no matches are found; otherwise, it returns the first matching element
2157      */
2158     @SuppressWarnings("unchecked")
2159     public <N extends DomNode> N querySelector(final String selectors) {
2160         final DomNodeList<DomNode> list = querySelectorAll(selectors);
2161         if (!list.isEmpty()) {
2162             return (N) list.get(0);
2163         }
2164         return null;
2165     }
2166 
2167     /**
2168      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2169      *
2170      * Indicates if this node is currently attached to the page.
2171      * @return {@code true} if the page is one ancestor of the node.
2172      */
2173     public boolean isAttachedToPage() {
2174         return attachedToPage_;
2175     }
2176 
2177     /**
2178      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2179      *
2180      * Lifecycle method to support special processing for js method importNode.
2181      * @param doc the import target document
2182      * @see org.htmlunit.javascript.host.dom.Document#importNode(
2183      * org.htmlunit.javascript.host.dom.Node, boolean)
2184      * @see HtmlScript#processImportNode(org.htmlunit.javascript.host.dom.Document)
2185      */
2186     public void processImportNode(final org.htmlunit.javascript.host.dom.Document doc) {
2187         page_ = (SgmlPage) doc.getDomNodeOrDie();
2188     }
2189 
2190     /**
2191      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
2192      *
2193      * Helper for a common call sequence.
2194      * @param feature the feature to check
2195      * @return {@code true} if the currently emulated browser has this feature.
2196      */
2197     public boolean hasFeature(final BrowserVersionFeatures feature) {
2198         return getPage().getWebClient().getBrowserVersion().hasFeature(feature);
2199     }
2200 
2201     private static final class CheckErrorHandler implements CSSErrorHandler {
2202         private CSSParseException error_;
2203 
2204         CSSParseException error() {
2205             return error_;
2206         }
2207 
2208         @Override
2209         public void warning(final CSSParseException exception) throws CSSException {
2210             // ignore
2211         }
2212 
2213         @Override
2214         public void fatalError(final CSSParseException exception) throws CSSException {
2215             error_ = exception;
2216         }
2217 
2218         @Override
2219         public void error(final CSSParseException exception) throws CSSException {
2220             error_ = exception;
2221         }
2222     }
2223 
2224     /**
2225      * Indicates if the provided event can be applied to this node.
2226      * Overwrite this.
2227      * @param event the event
2228      * @return {@code false} if the event can't be applied
2229      */
2230     public boolean handles(final Event event) {
2231         return true;
2232     }
2233 
2234     /**
2235      * Returns the previous sibling element node of this element.
2236      * null if this element has no element sibling nodes that come before this one in the document tree.
2237      * @return the previous sibling element node of this element.
2238      *         null if this element has no element sibling nodes that come before this one in the document tree
2239      */
2240     public DomElement getPreviousElementSibling() {
2241         DomNode node = getPreviousSibling();
2242         while (node != null && !(node instanceof DomElement)) {
2243             node = node.getPreviousSibling();
2244         }
2245         return (DomElement) node;
2246     }
2247 
2248     /**
2249      * Returns the next sibling element node of this element.
2250      * null if this element has no element sibling nodes that come after this one in the document tree.
2251      * @return the next sibling element node of this element.
2252      *         null if this element has no element sibling nodes that come after this one in the document tree
2253      */
2254     public DomElement getNextElementSibling() {
2255         DomNode node = getNextSibling();
2256         while (node != null && !(node instanceof DomElement)) {
2257             node = node.getNextSibling();
2258         }
2259         return (DomElement) node;
2260     }
2261 
2262     /**
2263      * @param selectorString the selector to test
2264      * @return the selected {@link DomElement} or null.
2265      */
2266     public DomElement closest(final String selectorString) {
2267         try {
2268             final WebClient webClient = getPage().getWebClient();
2269             final SelectorList selectorList = getSelectorList(selectorString, webClient);
2270 
2271             DomNode current = this;
2272             if (selectorList != null) {
2273                 do {
2274                     for (final Selector selector : selectorList) {
2275                         final DomElement elem = (DomElement) current;
2276                         if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, elem, null, true, true)) {
2277                             return elem;
2278                         }
2279                     }
2280 
2281                     do {
2282                         current = current.getParentNode();
2283                     }
2284                     while (current != null && !(current instanceof DomElement));
2285                 }
2286                 while (current != null);
2287             }
2288             return null;
2289         }
2290         catch (final IOException e) {
2291             throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage(), e);
2292         }
2293     }
2294 
2295     /**
2296      * An unmodifiable empty {@link NamedNodeMap} implementation.
2297      */
2298     private static final class ReadOnlyEmptyNamedNodeMapImpl implements NamedNodeMap, Serializable {
2299         private ReadOnlyEmptyNamedNodeMapImpl() {
2300             super();
2301         }
2302 
2303         /**
2304          * {@inheritDoc}
2305          */
2306         @Override
2307         public int getLength() {
2308             return 0;
2309         }
2310 
2311         /**
2312          * {@inheritDoc}
2313          */
2314         @Override
2315         public DomAttr getNamedItem(final String name) {
2316             return null;
2317         }
2318 
2319         /**
2320          * {@inheritDoc}
2321          */
2322         @Override
2323         public Node getNamedItemNS(final String namespaceURI, final String localName) {
2324             return null;
2325         }
2326 
2327         /**
2328          * {@inheritDoc}
2329          */
2330         @Override
2331         public Node item(final int index) {
2332             return null;
2333         }
2334 
2335         /**
2336          * {@inheritDoc}
2337          */
2338         @Override
2339         public Node removeNamedItem(final String name) throws DOMException {
2340             return null;
2341         }
2342 
2343         /**
2344          * {@inheritDoc}
2345          */
2346         @Override
2347         public Node removeNamedItemNS(final String namespaceURI, final String localName) {
2348             return null;
2349         }
2350 
2351         /**
2352          * {@inheritDoc}
2353          */
2354         @Override
2355         public DomAttr setNamedItem(final Node node) {
2356             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItem");
2357         }
2358 
2359         /**
2360          * {@inheritDoc}
2361          */
2362         @Override
2363         public Node setNamedItemNS(final Node node) throws DOMException {
2364             throw new UnsupportedOperationException("ReadOnlyEmptyNamedAttrNodeMapImpl.setNamedItemNS");
2365         }
2366     }
2367 }