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.css;
16  
17  import static java.nio.charset.StandardCharsets.UTF_8;
18  import static org.htmlunit.BrowserVersionFeatures.HTMLLINK_CHECK_TYPE_FOR_STYLESHEET;
19  import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.Reader;
25  import java.io.Serializable;
26  import java.io.StringReader;
27  import java.net.URL;
28  import java.nio.charset.Charset;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.regex.Pattern;
39  
40  import org.apache.commons.io.IOUtils;
41  import org.apache.commons.lang3.math.NumberUtils;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.htmlunit.BrowserVersion;
45  import org.htmlunit.Cache;
46  import org.htmlunit.FailingHttpStatusCodeException;
47  import org.htmlunit.Page;
48  import org.htmlunit.SgmlPage;
49  import org.htmlunit.WebClient;
50  import org.htmlunit.WebClient.PooledCSS3Parser;
51  import org.htmlunit.WebRequest;
52  import org.htmlunit.WebResponse;
53  import org.htmlunit.WebWindow;
54  import org.htmlunit.cssparser.dom.AbstractCSSRuleImpl;
55  import org.htmlunit.cssparser.dom.CSSImportRuleImpl;
56  import org.htmlunit.cssparser.dom.CSSMediaRuleImpl;
57  import org.htmlunit.cssparser.dom.CSSRuleListImpl;
58  import org.htmlunit.cssparser.dom.CSSStyleDeclarationImpl;
59  import org.htmlunit.cssparser.dom.CSSStyleRuleImpl;
60  import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
61  import org.htmlunit.cssparser.dom.CSSValueImpl;
62  import org.htmlunit.cssparser.dom.CSSValueImpl.CSSPrimitiveValueType;
63  import org.htmlunit.cssparser.dom.MediaListImpl;
64  import org.htmlunit.cssparser.dom.Property;
65  import org.htmlunit.cssparser.parser.CSSErrorHandler;
66  import org.htmlunit.cssparser.parser.CSSException;
67  import org.htmlunit.cssparser.parser.CSSOMParser;
68  import org.htmlunit.cssparser.parser.InputSource;
69  import org.htmlunit.cssparser.parser.LexicalUnit;
70  import org.htmlunit.cssparser.parser.condition.AttributeCondition;
71  import org.htmlunit.cssparser.parser.condition.Condition;
72  import org.htmlunit.cssparser.parser.condition.Condition.ConditionType;
73  import org.htmlunit.cssparser.parser.condition.HasPseudoClassCondition;
74  import org.htmlunit.cssparser.parser.condition.IsPseudoClassCondition;
75  import org.htmlunit.cssparser.parser.condition.NotPseudoClassCondition;
76  import org.htmlunit.cssparser.parser.condition.WherePseudoClassCondition;
77  import org.htmlunit.cssparser.parser.media.MediaQuery;
78  import org.htmlunit.cssparser.parser.selector.ChildSelector;
79  import org.htmlunit.cssparser.parser.selector.DescendantSelector;
80  import org.htmlunit.cssparser.parser.selector.DirectAdjacentSelector;
81  import org.htmlunit.cssparser.parser.selector.ElementSelector;
82  import org.htmlunit.cssparser.parser.selector.GeneralAdjacentSelector;
83  import org.htmlunit.cssparser.parser.selector.PseudoElementSelector;
84  import org.htmlunit.cssparser.parser.selector.RelativeSelector;
85  import org.htmlunit.cssparser.parser.selector.Selector;
86  import org.htmlunit.cssparser.parser.selector.Selector.SelectorType;
87  import org.htmlunit.cssparser.parser.selector.SelectorList;
88  import org.htmlunit.cssparser.parser.selector.SimpleSelector;
89  import org.htmlunit.html.DisabledElement;
90  import org.htmlunit.html.DomElement;
91  import org.htmlunit.html.DomNode;
92  import org.htmlunit.html.DomText;
93  import org.htmlunit.html.HtmlCheckBoxInput;
94  import org.htmlunit.html.HtmlElement;
95  import org.htmlunit.html.HtmlForm;
96  import org.htmlunit.html.HtmlInput;
97  import org.htmlunit.html.HtmlLink;
98  import org.htmlunit.html.HtmlOption;
99  import org.htmlunit.html.HtmlPage;
100 import org.htmlunit.html.HtmlRadioButtonInput;
101 import org.htmlunit.html.HtmlStyle;
102 import org.htmlunit.html.HtmlTextArea;
103 import org.htmlunit.html.ValidatableElement;
104 import org.htmlunit.javascript.host.css.MediaList;
105 import org.htmlunit.util.MimeType;
106 import org.htmlunit.util.StringUtils;
107 import org.htmlunit.util.UrlUtils;
108 
109 /**
110  * A css StyleSheet.
111  *
112  * @author Marc Guillemot
113  * @author Daniel Gredler
114  * @author Ahmed Ashour
115  * @author Ronald Brill
116  * @author Guy Burton
117  * @author Frank Danek
118  * @author Carsten Steul
119  * @author Sven Strickroth
120  */
121 public class CssStyleSheet implements Serializable {
122 
123     /** "none". */
124     public static final String NONE = "none";
125     /** "auto". */
126     public static final String AUTO = "auto";
127     /** "static". */
128     public static final String STATIC = "static";
129     /** "inherit". */
130     public static final String INHERIT = "inherit";
131     /** "initial". */
132     public static final String INITIAL = "initial";
133     /** "relative". */
134     public static final String RELATIVE = "relative";
135     /** "fixed". */
136     public static final String FIXED = "fixed";
137     /** "absolute". */
138     public static final String ABSOLUTE = "absolute";
139     /** "repeat". */
140     public static final String REPEAT = "repeat";
141     /** "block". */
142     public static final String BLOCK = "block";
143     /** "inline". */
144     public static final String INLINE = "inline";
145     /** "scroll". */
146     public static final String SCROLL = "scroll";
147 
148     private static final Log LOG = LogFactory.getLog(CssStyleSheet.class);
149 
150     private static final Pattern NTH_NUMERIC = Pattern.compile("\\d+");
151     private static final Pattern NTH_COMPLEX = Pattern.compile("[+-]?\\d*n\\w*([+-]\\w\\d*)?");
152     private static final Pattern UNESCAPE_SELECTOR = Pattern.compile("\\\\([\\[\\].:])");
153 
154     /** The parsed stylesheet which this host object wraps. */
155     private final CSSStyleSheetImpl wrapped_;
156 
157     /** The HTML element which owns this stylesheet. */
158     private final HtmlElement owner_;
159 
160     /** The CSS import rules and their corresponding stylesheets. */
161     private final Map<CSSImportRuleImpl, CssStyleSheet> imports_ = new HashMap<>();
162 
163     /** cache parsed media strings */
164     private static final Map<String, MediaListImpl> MEDIA = new HashMap<>();
165 
166     /** This stylesheet's URI (used to resolved contained @import rules). */
167     private final String uri_;
168 
169     private boolean enabled_ = true;
170 
171     /**
172      * Set of CSS2 pseudo class names.
173      */
174     public static final Set<String> CSS2_PSEUDO_CLASSES;
175 
176     private static final Set<String> CSS3_PSEUDO_CLASSES;
177 
178     /**
179      * Set of CSS4 pseudo class names.
180      */
181     public static final Set<String> CSS4_PSEUDO_CLASSES;
182 
183     static {
184         CSS2_PSEUDO_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
185                 "link", "visited", "hover", "active", "focus", "lang", "first-child")));
186 
187         final Set<String> css3 = new HashSet<>(Arrays.asList(
188                 "checked", "disabled", "enabled", "indeterminated", "root", "target", "not()",
189                 "nth-child()", "nth-last-child()", "nth-of-type()", "nth-last-of-type()",
190                 "last-child", "first-of-type", "last-of-type", "only-child", "only-of-type", "empty",
191                 "optional", "required", "valid", "invalid"));
192         css3.addAll(CSS2_PSEUDO_CLASSES);
193         CSS3_PSEUDO_CLASSES = Collections.unmodifiableSet(css3);
194 
195         final Set<String> css4 = new HashSet<>(Arrays.asList(
196                 // only what is supported at the moment
197                 "focus-within", "focus-visible"));
198         css4.addAll(CSS3_PSEUDO_CLASSES);
199         CSS4_PSEUDO_CLASSES = Collections.unmodifiableSet(css4);
200     }
201 
202     /**
203      * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
204      * @param element the owning node
205      * @param source the input source which contains the CSS stylesheet which this stylesheet host object represents
206      * @param uri this stylesheet's URI (used to resolved contained @import rules)
207      */
208     public CssStyleSheet(final HtmlElement element, final InputSource source, final String uri) {
209         if (source == null) {
210             wrapped_ = new CSSStyleSheetImpl();
211         }
212         else {
213             source.setURI(uri);
214             wrapped_ = parseCSS(source, element.getPage().getWebClient());
215         }
216         uri_ = uri;
217         owner_ = element;
218     }
219 
220     /**
221      * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
222      * @param element the owning node
223      * @param styleSheet the source which contains the CSS stylesheet which this stylesheet host object represents
224      * @param uri this stylesheet's URI (used to resolved contained @import rules)
225      */
226     public CssStyleSheet(final HtmlElement element, final String styleSheet, final String uri) {
227         CSSStyleSheetImpl css = null;
228         try (InputSource source = new InputSource(new StringReader(styleSheet))) {
229             source.setURI(uri);
230             css = parseCSS(source, element.getPage().getWebClient());
231         }
232         catch (final IOException e) {
233             LOG.error(e.getMessage(), e);
234         }
235 
236         wrapped_ = css;
237         uri_ = uri;
238         owner_ = element;
239     }
240 
241     /**
242      * Creates a new stylesheet representing the specified CSS stylesheet.
243      * @param element the owning node
244      * @param wrapped the CSS stylesheet which this stylesheet host object represents
245      * @param uri this stylesheet's URI (used to resolved contained @import rules)
246      */
247     public CssStyleSheet(final HtmlElement element, final CSSStyleSheetImpl wrapped, final String uri) {
248         wrapped_ = wrapped;
249         uri_ = uri;
250         owner_ = element;
251     }
252 
253     /**
254      * Returns the wrapped stylesheet.
255      * @return the wrapped stylesheet
256      */
257     public CSSStyleSheetImpl getWrappedSheet() {
258         return wrapped_;
259     }
260 
261     /**
262      * Returns this stylesheet's URI (used to resolved contained @import rules).
263      * For inline styles this is the page uri.
264      * @return this stylesheet's URI (used to resolved contained @import rules)
265      */
266     public String getUri() {
267         return uri_;
268     }
269 
270     /**
271      * Returns {@code true} if this stylesheet is enabled.
272      * @return {@code true} if this stylesheet is enabled
273      */
274     public boolean isEnabled() {
275         return enabled_;
276     }
277 
278     /**
279      * Sets whether this sheet is enabled or not.
280      * @param enabled enabled or not
281      */
282     public void setEnabled(final boolean enabled) {
283         enabled_ = enabled;
284     }
285 
286     /**
287      * Loads the stylesheet at the specified link or href.
288      * @param element the parent DOM element
289      * @param link the stylesheet's link (maybe {@code null} if a <code>url</code> is specified)
290      * @param url the stylesheet's url (maybe {@code null} if a <code>link</code> is specified)
291      * @return the loaded stylesheet
292      */
293     public static CssStyleSheet loadStylesheet(final HtmlElement element, final HtmlLink link, final String url) {
294         final HtmlPage page = (HtmlPage) element.getPage();
295         String uri = page.getUrl().toExternalForm();
296         try {
297             // Retrieve the associated content and respect client settings regarding failing HTTP status codes.
298             final WebRequest request;
299             final WebResponse response;
300             final WebClient client = page.getWebClient();
301             if (link == null) {
302                 // Use href.
303                 final BrowserVersion browser = client.getBrowserVersion();
304                 request = new WebRequest(new URL(url), browser.getCssAcceptHeader(), browser.getAcceptEncodingHeader());
305                 request.setRefererHeader(page.getUrl());
306                 // https://www.w3.org/TR/css-syntax-3/#input-byte-stream
307                 request.setDefaultResponseContentCharset(UTF_8);
308 
309                 // our cache is a bit strange;
310                 // loadWebResponse check the cache for the web response
311                 // AND also fixes the request url for the following cache lookups
312                 response = client.loadWebResponse(request);
313             }
314             else {
315                 // Use link.
316                 request = link.getWebRequest();
317 
318                 final String type = link.getTypeAttribute();
319                 if (client.getBrowserVersion().hasFeature(HTMLLINK_CHECK_TYPE_FOR_STYLESHEET)) {
320                     if (StringUtils.isNotBlank(type) && !MimeType.TEXT_CSS.equals(type)) {
321                         return new CssStyleSheet(element, "", uri);
322                     }
323                 }
324 
325                 if (request.getCharset() != null) {
326                     request.setDefaultResponseContentCharset(request.getCharset());
327                 }
328                 else {
329                     // https://www.w3.org/TR/css-syntax-3/#input-byte-stream
330                     request.setDefaultResponseContentCharset(UTF_8);
331                 }
332 
333                 // our cache is a bit strange;
334                 // loadWebResponse check the cache for the web response
335                 // AND also fixes the request url for the following cache lookups
336                 response = link.getWebResponse(true, request, true, type);
337                 if (response == null) {
338                     return new CssStyleSheet(element, "", uri);
339                 }
340             }
341 
342             // now we can look into the cache with the fixed request for
343             // a cached style sheet
344             final Cache cache = client.getCache();
345             final Object fromCache = cache.getCachedObject(request);
346             if (fromCache instanceof CSSStyleSheetImpl) {
347                 uri = request.getUrl().toExternalForm();
348                 return new CssStyleSheet(element, (CSSStyleSheetImpl) fromCache, uri);
349             }
350 
351             uri = response.getWebRequest().getUrl().toExternalForm();
352             client.printContentIfNecessary(response);
353             client.throwFailingHttpStatusCodeExceptionIfNecessary(response);
354             // CSS content must have downloaded OK; go ahead and build the corresponding stylesheet.
355 
356             final CssStyleSheet sheet;
357             final String contentType = response.getContentType();
358             if (StringUtils.isEmptyOrNull(contentType) || MimeType.TEXT_CSS.equals(contentType)) {
359                 try (InputStream in = response.getContentAsStreamWithBomIfApplicable()) {
360                     if (in == null) {
361                         if (LOG.isWarnEnabled()) {
362                             LOG.warn("Loading stylesheet for url '" + uri + "' returns empty responseData");
363                         }
364                         return new CssStyleSheet(element, "", uri);
365                     }
366 
367                     final Charset cssEncoding2 = response.getContentCharset();
368                     try (InputSource source = new InputSource(new InputStreamReader(in, cssEncoding2))) {
369                         source.setURI(uri);
370                         sheet = new CssStyleSheet(element, source, uri);
371                     }
372                 }
373             }
374             else {
375                 sheet = new CssStyleSheet(element, "", uri);
376             }
377 
378             // cache the style sheet
379             if (!cache.cacheIfPossible(request, response, sheet.getWrappedSheet())) {
380                 response.cleanUp();
381             }
382 
383             return sheet;
384         }
385         catch (final FailingHttpStatusCodeException e) {
386             // Got a 404 response or something like that; behave nicely.
387             if (LOG.isErrorEnabled()) {
388                 LOG.error("Exception loading " + uri, e);
389             }
390             return new CssStyleSheet(element, "", uri);
391         }
392         catch (final IOException e) {
393             // Got a basic IO error; behave nicely.
394             if (LOG.isErrorEnabled()) {
395                 LOG.error("IOException loading " + uri, e);
396             }
397             return new CssStyleSheet(element, "", uri);
398         }
399     }
400 
401     /**
402      * Returns {@code true} if the specified selector selects the specified element.
403      *
404      * @param browserVersion the browser version
405      * @param selector the selector to test
406      * @param element the element to test
407      * @param pseudoElement the pseudo element to match, (can be {@code null})
408      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)}
409      * @param throwOnSyntax throw exception if the selector syntax is incorrect
410      * @return {@code true} if it does apply, {@code false} if it doesn't apply
411      */
412     public static boolean selects(final BrowserVersion browserVersion, final Selector selector,
413             final DomElement element, final String pseudoElement, final boolean fromQuerySelectorAll,
414             final boolean throwOnSyntax) {
415         switch (selector.getSelectorType()) {
416             case ELEMENT_NODE_SELECTOR:
417                 final ElementSelector es = (ElementSelector) selector;
418 
419                 final String name;
420                 final String elementName;
421                 if (element.getPage().hasCaseSensitiveTagNames()) {
422                     name = es.getLocalName();
423                     elementName = element.getLocalName();
424                 }
425                 else {
426                     name = es.getLocalNameLowerCase();
427                     elementName = element.getLowercaseName();
428                 }
429 
430                 if (name == null || name.equals(elementName)) {
431                     final List<Condition> conditions = es.getConditions();
432                     if (conditions != null) {
433                         for (final Condition condition : conditions) {
434                             if (!selects(browserVersion, condition, element, fromQuerySelectorAll, throwOnSyntax)) {
435                                 return false;
436                             }
437                         }
438                     }
439                     return true;
440                 }
441 
442                 return false;
443 
444             case CHILD_SELECTOR:
445                 final DomNode parentNode = element.getParentNode();
446                 if (parentNode == element.getPage()) {
447                     return false;
448                 }
449                 if (!(parentNode instanceof DomElement)) {
450                     return false; // for instance parent is a DocumentFragment
451                 }
452                 final ChildSelector cs = (ChildSelector) selector;
453                 return selects(browserVersion, cs.getSimpleSelector(), element, pseudoElement,
454                             fromQuerySelectorAll, throwOnSyntax)
455                     && selects(browserVersion, cs.getAncestorSelector(), (DomElement) parentNode,
456                             pseudoElement, fromQuerySelectorAll, throwOnSyntax);
457 
458             case DESCENDANT_SELECTOR:
459                 final DescendantSelector ds = (DescendantSelector) selector;
460                 final SimpleSelector simpleSelector = ds.getSimpleSelector();
461                 if (selects(browserVersion, simpleSelector, element, pseudoElement,
462                             fromQuerySelectorAll, throwOnSyntax)) {
463                     DomNode ancestor = element;
464                     if (simpleSelector.getSelectorType() != SelectorType.PSEUDO_ELEMENT_SELECTOR) {
465                         ancestor = ancestor.getParentNode();
466                     }
467                     final Selector dsAncestorSelector = ds.getAncestorSelector();
468                     while (ancestor instanceof DomElement) {
469                         if (selects(browserVersion, dsAncestorSelector, (DomElement) ancestor, pseudoElement,
470                                 fromQuerySelectorAll, throwOnSyntax)) {
471                             return true;
472                         }
473                         ancestor = ancestor.getParentNode();
474                     }
475                 }
476                 return false;
477 
478             case DIRECT_ADJACENT_SELECTOR:
479                 final DirectAdjacentSelector das = (DirectAdjacentSelector) selector;
480                 if (selects(browserVersion, das.getSimpleSelector(), element, pseudoElement,
481                             fromQuerySelectorAll, throwOnSyntax)) {
482                     DomNode prev = element.getPreviousSibling();
483                     while (prev != null && !(prev instanceof DomElement)) {
484                         prev = prev.getPreviousSibling();
485                     }
486                     return prev != null
487                             && selects(browserVersion, das.getSelector(),
488                                     (DomElement) prev, pseudoElement, fromQuerySelectorAll, throwOnSyntax);
489                 }
490                 return false;
491 
492             case GENERAL_ADJACENT_SELECTOR:
493                 final GeneralAdjacentSelector gas = (GeneralAdjacentSelector) selector;
494                 if (selects(browserVersion, gas.getSimpleSelector(), element, pseudoElement,
495                             fromQuerySelectorAll, throwOnSyntax)) {
496                     for (DomNode prev1 = element.getPreviousSibling(); prev1 != null;
497                                                         prev1 = prev1.getPreviousSibling()) {
498                         if (prev1 instanceof DomElement
499                             && selects(browserVersion, gas.getSelector(), (DomElement) prev1,
500                                     pseudoElement, fromQuerySelectorAll, throwOnSyntax)) {
501                             return true;
502                         }
503                     }
504                 }
505                 return false;
506             case PSEUDO_ELEMENT_SELECTOR:
507                 if (pseudoElement != null && pseudoElement.length() != 0 && pseudoElement.charAt(0) == ':') {
508                     final String pseudoName = ((PseudoElementSelector) selector).getLocalName();
509                     return pseudoName.equals(pseudoElement.substring(1));
510                 }
511                 return false;
512 
513             case RELATIVE_SELECTOR:
514                 final RelativeSelector rs = (RelativeSelector) selector;
515 
516                 switch (rs.getCombinator()) {
517                     case DESCENDANT_COMBINATOR:
518                         for (final DomElement descendant : element.getDomElementDescendants()) {
519                             if (selects(browserVersion, rs.getSelector(), descendant, pseudoElement,
520                                             fromQuerySelectorAll, throwOnSyntax)) {
521                                 return true;
522                             }
523                         }
524                         return false;
525 
526                     case CHILD_COMBINATOR:
527                         for (final DomElement child : element.getChildElements()) {
528                             if (selects(browserVersion, rs.getSelector(), child, pseudoElement,
529                                             fromQuerySelectorAll, throwOnSyntax)) {
530                                 return true;
531                             }
532                         }
533                         return false;
534 
535                     case NEXT_SIBLING_COMBINATOR:
536                         final DomElement nextSibling = element.getNextElementSibling();
537                         if (selects(browserVersion, rs.getSelector(), nextSibling, pseudoElement,
538                                             fromQuerySelectorAll, throwOnSyntax)) {
539                             return true;
540                         }
541                         return false;
542 
543                     case SUBSEQUENT_SIBLING_COMBINATOR:
544                         for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
545                             if (n instanceof DomElement
546                                     && selects(browserVersion, rs.getSelector(), (DomElement) n, pseudoElement,
547                                                 fromQuerySelectorAll, throwOnSyntax)) {
548                                 return true;
549                             }
550                         }
551                         return false;
552 
553                     default:
554                         if (LOG.isErrorEnabled()) {
555                             LOG.error("Unknown CSS combinator '" + rs.getCombinator() + "'.");
556                         }
557                         return false;
558                 }
559 
560             default:
561                 if (LOG.isErrorEnabled()) {
562                     LOG.error("Unknown CSS selector type '" + selector.getSelectorType() + "'.");
563                 }
564                 return false;
565         }
566     }
567 
568     /**
569      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
570      * Returns {@code true} if the specified condition selects the specified element.
571      *
572      * @param browserVersion the browser version
573      * @param condition the condition to test
574      * @param element the element to test
575      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)}
576      * @param throwOnSyntax throw exception if the selector syntax is incorrect
577      * @return {@code true} if it does apply, {@code false} if it doesn't apply
578      */
579     static boolean selects(final BrowserVersion browserVersion,
580             final Condition condition, final DomElement element,
581             final boolean fromQuerySelectorAll, final boolean throwOnSyntax) {
582 
583         switch (condition.getConditionType()) {
584             case ID_CONDITION:
585                 return condition.getValue().equals(element.getId());
586 
587             case CLASS_CONDITION:
588                 String v3 = condition.getValue();
589                 if (v3.indexOf('\\') > -1) {
590                     v3 = UNESCAPE_SELECTOR.matcher(v3).replaceAll("$1");
591                 }
592                 final String a3 = element.getAttributeDirect("class");
593                 return selectsWhitespaceSeparated(v3, a3);
594 
595             case ATTRIBUTE_CONDITION:
596                 final AttributeCondition attributeCondition = (AttributeCondition) condition;
597                 String value = attributeCondition.getValue();
598                 if (value != null) {
599                     if (value.indexOf('\\') > -1) {
600                         value = UNESCAPE_SELECTOR.matcher(value).replaceAll("$1");
601                     }
602                     final String name = attributeCondition.getLocalName();
603                     final String attrValue = element.getAttribute(name);
604                     if (attributeCondition.isCaseInSensitive() || DomElement.TYPE_ATTRIBUTE.equals(name)) {
605                         return ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equalsIgnoreCase(value);
606                     }
607                     return ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equals(value);
608                 }
609                 return element.hasAttribute(condition.getLocalName());
610 
611             case PREFIX_ATTRIBUTE_CONDITION:
612                 final AttributeCondition prefixAttributeCondition = (AttributeCondition) condition;
613                 final String prefixValue = prefixAttributeCondition.getValue();
614                 if (prefixAttributeCondition.isCaseInSensitive()) {
615                     return !StringUtils.isEmptyString(prefixValue)
616                             && StringUtils.startsWithIgnoreCase(
617                                     element.getAttribute(prefixAttributeCondition.getLocalName()), prefixValue);
618                 }
619                 return !StringUtils.isEmptyString(prefixValue)
620                         && element.getAttribute(prefixAttributeCondition.getLocalName()).startsWith(prefixValue);
621 
622             case SUFFIX_ATTRIBUTE_CONDITION:
623                 final AttributeCondition suffixAttributeCondition = (AttributeCondition) condition;
624                 final String suffixValue = suffixAttributeCondition.getValue();
625                 if (suffixAttributeCondition.isCaseInSensitive()) {
626                     return !StringUtils.isEmptyString(suffixValue)
627                             && StringUtils.endsWithIgnoreCase(
628                                     element.getAttribute(suffixAttributeCondition.getLocalName()), suffixValue);
629                 }
630                 return !StringUtils.isEmptyString(suffixValue)
631                         && element.getAttribute(suffixAttributeCondition.getLocalName()).endsWith(suffixValue);
632 
633             case SUBSTRING_ATTRIBUTE_CONDITION:
634                 final AttributeCondition substringAttributeCondition = (AttributeCondition) condition;
635                 final String substringValue = substringAttributeCondition.getValue();
636                 if (substringAttributeCondition.isCaseInSensitive()) {
637                     return !StringUtils.isEmptyString(substringValue)
638                             && StringUtils.containsIgnoreCase(
639                                     element.getAttribute(substringAttributeCondition.getLocalName()), substringValue);
640                 }
641                 return !StringUtils.isEmptyString(substringValue)
642                         && element.getAttribute(substringAttributeCondition.getLocalName()).contains(substringValue);
643 
644             case BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
645                 final AttributeCondition beginHyphenAttributeCondition = (AttributeCondition) condition;
646                 final String v = beginHyphenAttributeCondition.getValue();
647                 final String a = element.getAttribute(beginHyphenAttributeCondition.getLocalName());
648                 if (beginHyphenAttributeCondition.isCaseInSensitive()) {
649                     return selectsHyphenSeparated(
650                             StringUtils.toRootLowerCase(v),
651                             StringUtils.toRootLowerCase(a));
652                 }
653                 return selectsHyphenSeparated(v, a);
654 
655             case ONE_OF_ATTRIBUTE_CONDITION:
656                 final AttributeCondition oneOfAttributeCondition = (AttributeCondition) condition;
657                 final String v2 = oneOfAttributeCondition.getValue();
658                 final String a2 = element.getAttribute(oneOfAttributeCondition.getLocalName());
659                 if (oneOfAttributeCondition.isCaseInSensitive()) {
660                     return selectsOneOf(
661                             StringUtils.toRootLowerCase(v2),
662                             StringUtils.toRootLowerCase(a2));
663                 }
664                 return selectsOneOf(v2, a2);
665 
666             case LANG_CONDITION:
667                 final String lcLang = condition.getValue();
668                 final int lcLangLength = lcLang.length();
669                 for (DomNode node = element; node instanceof HtmlElement; node = node.getParentNode()) {
670                     final String nodeLang = ((HtmlElement) node).getAttributeDirect("lang");
671                     if (ATTRIBUTE_NOT_DEFINED != nodeLang) {
672                         // "en", "en-GB" should be matched by "en" but not "english"
673                         return nodeLang.startsWith(lcLang)
674                             && (nodeLang.length() == lcLangLength || '-' == nodeLang.charAt(lcLangLength));
675                     }
676                 }
677                 return false;
678 
679             case NOT_PSEUDO_CLASS_CONDITION:
680                 final NotPseudoClassCondition notPseudoCondition = (NotPseudoClassCondition) condition;
681                 final SelectorList notSelectorList = notPseudoCondition.getSelectors();
682                 for (final Selector selector : notSelectorList) {
683                     if (selects(browserVersion, selector, element, null, fromQuerySelectorAll, throwOnSyntax)) {
684                         return false;
685                     }
686                 }
687                 return true;
688 
689             case IS_PSEUDO_CLASS_CONDITION:
690                 final IsPseudoClassCondition conditionIsPseudo = (IsPseudoClassCondition) condition;
691                 for (final Selector selector : conditionIsPseudo.getSelectors()) {
692                     if (selects(browserVersion, selector, element, null, fromQuerySelectorAll, throwOnSyntax)) {
693                         return true;
694                     }
695                 }
696                 return false;
697 
698             case WHERE_PSEUDO_CLASS_CONDITION:
699                 // same as is
700                 final WherePseudoClassCondition conditionWherePseudo = (WherePseudoClassCondition) condition;
701                 for (final Selector selector : conditionWherePseudo.getSelectors()) {
702                     if (selects(browserVersion, selector, element, null, fromQuerySelectorAll, throwOnSyntax)) {
703                         return true;
704                     }
705                 }
706                 return false;
707 
708             case HAS_PSEUDO_CLASS_CONDITION:
709                 final HasPseudoClassCondition conditionHasPseudo = (HasPseudoClassCondition) condition;
710                 for (final Selector selector : conditionHasPseudo.getSelectors()) {
711                     if (selects(browserVersion, selector, element, null, fromQuerySelectorAll, throwOnSyntax)) {
712                         return true;
713                     }
714                 }
715                 return false;
716 
717             case PSEUDO_CLASS_CONDITION:
718                 return selectsPseudoClass(browserVersion, condition, element);
719 
720             default:
721                 if (LOG.isErrorEnabled()) {
722                     LOG.error("Unknown CSS condition type '" + condition.getConditionType() + "'.");
723                 }
724                 return false;
725         }
726     }
727 
728     private static boolean selectsOneOf(final String condition, final String attribute) {
729         // attribute.equals(condition)
730         // || attribute.startsWith(condition + " ") || attriubte.endsWith(" " + condition)
731         // || attribute.contains(" " + condition + " ");
732 
733         final int conditionLength = condition.length();
734         if (conditionLength < 1) {
735             return false;
736         }
737 
738         final int attribLength = attribute.length();
739         if (attribLength < conditionLength) {
740             return false;
741         }
742         if (attribLength > conditionLength) {
743             if (' ' == attribute.charAt(conditionLength)
744                     && attribute.startsWith(condition)) {
745                 return true;
746             }
747             if (' ' == attribute.charAt(attribLength - conditionLength - 1)
748                     && attribute.endsWith(condition)) {
749                 return true;
750             }
751             if (attribLength + 1 > conditionLength) {
752                 final StringBuilder tmp = new StringBuilder(conditionLength + 2);
753                 tmp.append(' ').append(condition).append(' ');
754                 return attribute.contains(tmp);
755             }
756             return false;
757         }
758         return attribute.equals(condition);
759     }
760 
761     private static boolean selectsHyphenSeparated(final String condition, final String attribute) {
762         final int conditionLength = condition.length();
763         if (conditionLength < 1) {
764             if (attribute != ATTRIBUTE_NOT_DEFINED) {
765                 final int attribLength = attribute.length();
766                 return attribLength == 0 || '-' == attribute.charAt(0);
767             }
768             return false;
769         }
770 
771         final int attribLength = attribute.length();
772         if (attribLength < conditionLength) {
773             return false;
774         }
775         if (attribLength > conditionLength) {
776             return '-' == attribute.charAt(conditionLength)
777                     && attribute.startsWith(condition);
778         }
779         return attribute.equals(condition);
780     }
781 
782     private static boolean selectsWhitespaceSeparated(final String condition, final String attribute) {
783         final int conditionLength = condition.length();
784         if (conditionLength < 1) {
785             return false;
786         }
787 
788         final int attribLength = attribute.length();
789         if (attribLength < conditionLength) {
790             return false;
791         }
792 
793         int pos = attribute.indexOf(condition);
794         while (pos != -1) {
795             if (pos > 0 && !Character.isWhitespace(attribute.charAt(pos - 1))) {
796                 pos = attribute.indexOf(condition, pos + 1);
797             }
798             else {
799                 final int lastPos = pos + condition.length();
800                 if (lastPos >= attribLength || Character.isWhitespace(attribute.charAt(lastPos))) {
801                     return true;
802                 }
803                 pos = attribute.indexOf(condition, pos + 1);
804             }
805         }
806 
807         return false;
808     }
809 
810     @SuppressWarnings("PMD.UselessParentheses")
811     private static boolean selectsPseudoClass(final BrowserVersion browserVersion,
812             final Condition condition, final DomElement element) {
813         final String value = condition.getValue();
814         switch (value) {
815             case "root":
816                 return element == element.getPage().getDocumentElement();
817 
818             case "enabled":
819                 return element instanceof DisabledElement && !((DisabledElement) element).isDisabled();
820 
821             case "disabled":
822                 return element instanceof DisabledElement && ((DisabledElement) element).isDisabled();
823 
824             case "focus":
825                 final HtmlPage htmlPage = element.getHtmlPageOrNull();
826                 if (htmlPage != null) {
827                     final DomElement focus = htmlPage.getFocusedElement();
828                     return element == focus;
829                 }
830                 return false;
831 
832             case "focus-within":
833                 final HtmlPage htmlPage2 = element.getHtmlPageOrNull();
834                 if (htmlPage2 != null) {
835                     final DomElement focus = htmlPage2.getFocusedElement();
836                     return element == focus || element.isAncestorOf(focus);
837                 }
838                 return false;
839 
840             case "focus-visible":
841                 final HtmlPage htmlPage3 = element.getHtmlPageOrNull();
842                 if (htmlPage3 != null) {
843                     final DomElement focus = htmlPage3.getFocusedElement();
844                     return element == focus
845                             && ((element instanceof HtmlInput && !((HtmlInput) element).isReadOnly())
846                                 || (element instanceof HtmlTextArea && !((HtmlTextArea) element).isReadOnly()));
847                 }
848                 return false;
849 
850             case "checked":
851                 return (element instanceof HtmlCheckBoxInput && ((HtmlCheckBoxInput) element).isChecked())
852                         || (element instanceof HtmlRadioButtonInput && ((HtmlRadioButtonInput) element).isChecked()
853                                 || (element instanceof HtmlOption && ((HtmlOption) element).isSelected()));
854 
855             case "required":
856                 return element instanceof HtmlElement && ((HtmlElement) element).isRequired();
857 
858             case "optional":
859                 return element instanceof HtmlElement && ((HtmlElement) element).isOptional();
860 
861             case "first-child":
862                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
863                     if (n instanceof DomElement) {
864                         return false;
865                     }
866                 }
867                 return true;
868 
869             case "last-child":
870                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
871                     if (n instanceof DomElement) {
872                         return false;
873                     }
874                 }
875                 return true;
876 
877             case "first-of-type":
878                 final String firstType = element.getNodeName();
879                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
880                     if (n instanceof DomElement && n.getNodeName().equals(firstType)) {
881                         return false;
882                     }
883                 }
884                 return true;
885 
886             case "last-of-type":
887                 final String lastType = element.getNodeName();
888                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
889                     if (n instanceof DomElement && n.getNodeName().equals(lastType)) {
890                         return false;
891                     }
892                 }
893                 return true;
894 
895             case "only-child":
896                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
897                     if (n instanceof DomElement) {
898                         return false;
899                     }
900                 }
901                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
902                     if (n instanceof DomElement) {
903                         return false;
904                     }
905                 }
906                 return true;
907 
908             case "only-of-type":
909                 final String type = element.getNodeName();
910                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
911                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
912                         return false;
913                     }
914                 }
915                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
916                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
917                         return false;
918                     }
919                 }
920                 return true;
921 
922             case "valid":
923                 if (element instanceof HtmlForm || element instanceof ValidatableElement) {
924                     return ((HtmlElement) element).isValid();
925                 }
926                 return false;
927 
928             case "invalid":
929                 if (element instanceof HtmlForm || element instanceof ValidatableElement) {
930                     return !((HtmlElement) element).isValid();
931                 }
932                 return false;
933 
934             case "empty":
935                 return isEmpty(element);
936 
937             case "target":
938                 final String ref = element.getPage().getUrl().getRef();
939                 return StringUtils.isNotBlank(ref) && ref.equals(element.getId());
940 
941             case "hover":
942                 return element.isMouseOver();
943 
944             case "placeholder-shown":
945                 return element instanceof HtmlInput
946                         && StringUtils.isEmptyOrNull(((HtmlInput) element).getValue())
947                         && !StringUtils.isEmptyOrNull(((HtmlInput) element).getPlaceholder());
948 
949             default:
950                 if (value.startsWith("nth-child(")) {
951                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
952                     int index = 0;
953                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
954                         if (n instanceof DomElement) {
955                             index++;
956                         }
957                     }
958                     return getNthElement(nth, index);
959                 }
960                 else if (value.startsWith("nth-last-child(")) {
961                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
962                     int index = 0;
963                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
964                         if (n instanceof DomElement) {
965                             index++;
966                         }
967                     }
968                     return getNthElement(nth, index);
969                 }
970                 else if (value.startsWith("nth-of-type(")) {
971                     final String nthType = element.getNodeName();
972                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
973                     int index = 0;
974                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
975                         if (n instanceof DomElement && n.getNodeName().equals(nthType)) {
976                             index++;
977                         }
978                     }
979                     return getNthElement(nth, index);
980                 }
981                 else if (value.startsWith("nth-last-of-type(")) {
982                     final String nthLastType = element.getNodeName();
983                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
984                     int index = 0;
985                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
986                         if (n instanceof DomElement && n.getNodeName().equals(nthLastType)) {
987                             index++;
988                         }
989                     }
990                     return getNthElement(nth, index);
991                 }
992                 return false;
993         }
994     }
995 
996     private static boolean isEmpty(final DomElement element) {
997         for (DomNode n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
998             if (n instanceof DomElement || n instanceof DomText) {
999                 return false;
1000             }
1001         }
1002         return true;
1003     }
1004 
1005     private static boolean getNthElement(final String nth, final int index) {
1006         if ("odd".equalsIgnoreCase(nth)) {
1007             return index % 2 != 0;
1008         }
1009 
1010         if ("even".equalsIgnoreCase(nth)) {
1011             return index % 2 == 0;
1012         }
1013 
1014         // (numerator) * n + (denominator)
1015         final int nIndex = nth.indexOf('n');
1016         int denominator = 0;
1017         if (nIndex != -1) {
1018             String value = nth.substring(0, nIndex).trim();
1019             if (StringUtils.equalsChar('-', value)) {
1020                 denominator = -1;
1021             }
1022             else {
1023                 if (value.length() > 0 && value.charAt(0) == '+') {
1024                     value = value.substring(1);
1025                 }
1026                 denominator = NumberUtils.toInt(value, 1);
1027             }
1028         }
1029 
1030         String value = nth.substring(nIndex + 1).trim();
1031         if (value.length() > 0 && value.charAt(0) == '+') {
1032             value = value.substring(1);
1033         }
1034         final int numerator = NumberUtils.toInt(value, 0);
1035         if (denominator == 0) {
1036             return index == numerator && numerator > 0;
1037         }
1038 
1039         final double n = (index - numerator) / (double) denominator;
1040         return n >= 0 && n % 1 == 0;
1041     }
1042 
1043     /**
1044      * Parses the CSS at the specified input source. If anything at all goes wrong, this method
1045      * returns an empty stylesheet.
1046      *
1047      * @param source the source from which to retrieve the CSS to be parsed
1048      * @param client the client
1049      * @return the stylesheet parsed from the specified input source
1050      */
1051     private static CSSStyleSheetImpl parseCSS(final InputSource source, final WebClient client) {
1052         CSSStyleSheetImpl ss;
1053 
1054         // use a pooled parser, if any available to avoid expensive recreation
1055         try (PooledCSS3Parser pooledParser = client.getCSS3Parser()) {
1056             final CSSErrorHandler errorHandler = client.getCssErrorHandler();
1057             final CSSOMParser parser = new CSSOMParser(pooledParser);
1058             parser.setErrorHandler(errorHandler);
1059             ss = parser.parseStyleSheet(source, null);
1060         }
1061         catch (final Throwable ex) {
1062             if (LOG.isErrorEnabled()) {
1063                 LOG.error("Error parsing CSS from '" + toString(source) + "': " + ex.getMessage(), ex);
1064             }
1065             ss = new CSSStyleSheetImpl();
1066         }
1067         return ss;
1068     }
1069 
1070     /**
1071      * Parses the given media string. If anything at all goes wrong, this
1072      * method returns an empty MediaList list.
1073      *
1074      * @param mediaString the source from which to retrieve the media to be parsed
1075      * @param webClient the {@link WebClient} to be used
1076      * @return the media parsed from the specified input source
1077      */
1078     public static MediaListImpl parseMedia(final String mediaString, final WebClient webClient) {
1079         MediaListImpl media = MEDIA.get(mediaString);
1080         if (media != null) {
1081             return media;
1082         }
1083 
1084         // get us a pooled parser for efficiency because a new parser is expensive
1085         try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
1086             final CSSOMParser parser = new CSSOMParser(pooledParser);
1087             parser.setErrorHandler(webClient.getCssErrorHandler());
1088 
1089             media = new MediaListImpl(parser.parseMedia(mediaString));
1090             MEDIA.put(mediaString, media);
1091             return media;
1092         }
1093         catch (final Exception e) {
1094             if (LOG.isErrorEnabled()) {
1095                 LOG.error("Error parsing CSS media from '" + mediaString + "': " + e.getMessage(), e);
1096             }
1097         }
1098 
1099         media = new MediaListImpl(null);
1100         MEDIA.put(mediaString, media);
1101         return media;
1102     }
1103 
1104     /**
1105      * Returns the contents of the specified input source, ignoring any {@link IOException}s.
1106      * @param source the input source from which to read
1107      * @return the contents of the specified input source, or an empty string if an {@link IOException} occurs
1108      */
1109     private static String toString(final InputSource source) {
1110         try {
1111             final Reader reader = source.getReader();
1112             if (null != reader) {
1113                 // try to reset to produce some output
1114                 if (reader instanceof StringReader) {
1115                     final StringReader sr = (StringReader) reader;
1116                     sr.reset();
1117                 }
1118                 return IOUtils.toString(reader);
1119             }
1120             return "";
1121         }
1122         catch (final IOException e) {
1123             LOG.error(e.getMessage(), e);
1124             return "";
1125         }
1126     }
1127 
1128     /**
1129      * Validates the list of selectors.
1130      * @param selectorList the selectors
1131      * @param domNode the dom node the query should work on
1132      *
1133      * @throws CSSException if a selector is invalid
1134      */
1135     public static void validateSelectors(final SelectorList selectorList, final DomNode domNode) throws CSSException {
1136         for (final Selector selector : selectorList) {
1137             if (!isValidSelector(selector, domNode)) {
1138                 throw new CSSException("Invalid selector: " + selector, null);
1139             }
1140         }
1141     }
1142 
1143     private static boolean isValidSelector(final Selector selector, final DomNode domNode) {
1144         switch (selector.getSelectorType()) {
1145             case ELEMENT_NODE_SELECTOR:
1146                 final List<Condition> conditions = ((ElementSelector) selector).getConditions();
1147                 if (conditions != null) {
1148                     for (final Condition condition : conditions) {
1149                         if (!isValidCondition(condition, domNode)) {
1150                             return false;
1151                         }
1152                     }
1153                 }
1154                 return true;
1155             case DESCENDANT_SELECTOR:
1156                 final DescendantSelector ds = (DescendantSelector) selector;
1157                 return isValidSelector(ds.getAncestorSelector(), domNode)
1158                         && isValidSelector(ds.getSimpleSelector(), domNode);
1159             case CHILD_SELECTOR:
1160                 final ChildSelector cs = (ChildSelector) selector;
1161                 return isValidSelector(cs.getAncestorSelector(), domNode)
1162                         && isValidSelector(cs.getSimpleSelector(), domNode);
1163             case DIRECT_ADJACENT_SELECTOR:
1164                 final DirectAdjacentSelector das = (DirectAdjacentSelector) selector;
1165                 return isValidSelector(das.getSelector(), domNode)
1166                         && isValidSelector(das.getSimpleSelector(), domNode);
1167             case GENERAL_ADJACENT_SELECTOR:
1168                 final GeneralAdjacentSelector gas = (GeneralAdjacentSelector) selector;
1169                 return isValidSelector(gas.getSelector(), domNode)
1170                         && isValidSelector(gas.getSimpleSelector(), domNode);
1171             case PSEUDO_ELEMENT_SELECTOR:
1172                 // as of now (4.17) the htmlunit-cssparser accepts only supported selectors
1173                 // if ("first-line".equals(s)
1174                 //         || "first-letter".equals(s)
1175                 //         || "before".equals(s)
1176                 //         || "after".equals(s))
1177                 //     {
1178                 //         return new PseudoElementSelector(s, locator, doubleColon);
1179                 //     }
1180                 return true;
1181             case RELATIVE_SELECTOR:
1182                 final RelativeSelector rs = (RelativeSelector) selector;
1183                 return isValidSelector(rs.getSelector(), domNode);
1184             default:
1185                 if (LOG.isWarnEnabled()) {
1186                     LOG.warn("Unhandled CSS selector type '"
1187                                 + selector.getSelectorType() + "'. Accepting it silently.");
1188                 }
1189                 return true; // at least in a first time to break less stuff
1190         }
1191     }
1192 
1193     private static boolean isValidCondition(final Condition condition, final DomNode domNode) {
1194         switch (condition.getConditionType()) {
1195             case ATTRIBUTE_CONDITION:
1196             case ID_CONDITION:
1197             case LANG_CONDITION:
1198             case ONE_OF_ATTRIBUTE_CONDITION:
1199             case BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
1200             case CLASS_CONDITION:
1201             case PREFIX_ATTRIBUTE_CONDITION:
1202             case SUBSTRING_ATTRIBUTE_CONDITION:
1203             case SUFFIX_ATTRIBUTE_CONDITION:
1204                 return true;
1205             case NOT_PSEUDO_CLASS_CONDITION:
1206                 final NotPseudoClassCondition notPseudoCondition = (NotPseudoClassCondition) condition;
1207                 final SelectorList notSelectorList = notPseudoCondition.getSelectors();
1208                 for (final Selector selector : notSelectorList) {
1209                     if (!isValidSelector(selector, domNode)) {
1210                         return false;
1211                     }
1212                 }
1213                 return true;
1214             case IS_PSEUDO_CLASS_CONDITION:
1215                 final IsPseudoClassCondition conditionIsPseudo = (IsPseudoClassCondition) condition;
1216                 for (final Selector selector : conditionIsPseudo.getSelectors()) {
1217                     if (!isValidSelector(selector, domNode)) {
1218                         return false;
1219                     }
1220                 }
1221                 return true;
1222             case WHERE_PSEUDO_CLASS_CONDITION:
1223                 final WherePseudoClassCondition conditionWherePseudo = (WherePseudoClassCondition) condition;
1224                 for (final Selector selector : conditionWherePseudo.getSelectors()) {
1225                     if (!isValidSelector(selector, domNode)) {
1226                         return false;
1227                     }
1228                 }
1229                 return true;
1230             case HAS_PSEUDO_CLASS_CONDITION:
1231                 final HasPseudoClassCondition conditionHasPseudo = (HasPseudoClassCondition) condition;
1232                 for (final Selector selector : conditionHasPseudo.getSelectors()) {
1233                     if (!isValidSelector(selector, domNode)) {
1234                         return false;
1235                     }
1236                 }
1237                 return true;
1238             case PSEUDO_CLASS_CONDITION:
1239                 String value = condition.getValue();
1240                 if (value.endsWith(")")) {
1241                     if (value.endsWith("()")) {
1242                         return false;
1243                     }
1244                     value = value.substring(0, value.indexOf('(') + 1) + ')';
1245                 }
1246 
1247                 if ("nth-child()".equals(value)) {
1248                     final String arg = org.apache.commons.lang3.StringUtils
1249                                         .substringBetween(condition.getValue(), "(", ")").trim();
1250                     return "even".equalsIgnoreCase(arg) || "odd".equalsIgnoreCase(arg)
1251                             || NTH_NUMERIC.matcher(arg).matches()
1252                             || NTH_COMPLEX.matcher(arg).matches();
1253                 }
1254 
1255                 if ("placeholder-shown".equals(value)) {
1256                     return true;
1257                 }
1258 
1259                 return CSS4_PSEUDO_CLASSES.contains(value);
1260             default:
1261                 if (LOG.isWarnEnabled()) {
1262                     LOG.warn("Unhandled CSS condition type '"
1263                                 + condition.getConditionType() + "'. Accepting it silently.");
1264                 }
1265                 return true;
1266         }
1267     }
1268 
1269     /**
1270      * @param importRule the {@link CSSImportRuleImpl} that imports the {@link CssStyleSheet}
1271      * @return the {@link CssStyleSheet} imported by this rule
1272      */
1273     public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
1274         CssStyleSheet sheet = imports_.get(importRule);
1275         if (sheet == null) {
1276             final String href = importRule.getHref();
1277             final String url = UrlUtils.resolveUrl(getUri(), href);
1278             sheet = loadStylesheet(owner_, null, url);
1279             imports_.put(importRule, sheet);
1280         }
1281         return sheet;
1282     }
1283 
1284     /**
1285      * Returns {@code true} if this stylesheet is active, based on the media types it is associated with (if any).
1286      * @return {@code true} if this stylesheet is active, based on the media types it is associated with (if any)
1287      */
1288     public boolean isActive() {
1289         final String media;
1290         if (owner_ instanceof HtmlStyle) {
1291             final HtmlStyle style = (HtmlStyle) owner_;
1292             media = style.getMediaAttribute();
1293         }
1294         else if (owner_ instanceof HtmlLink) {
1295             final HtmlLink link = (HtmlLink) owner_;
1296             media = link.getMediaAttribute();
1297         }
1298         else {
1299             return true;
1300         }
1301 
1302         if (StringUtils.isBlank(media)) {
1303             return true;
1304         }
1305 
1306         final WebWindow webWindow = owner_.getPage().getEnclosingWindow();
1307         final MediaListImpl mediaList = parseMedia(media, webWindow.getWebClient());
1308         return isActive(mediaList, webWindow);
1309     }
1310 
1311     /**
1312      * Returns whether the specified {@link MediaList} is active or not.
1313      * @param mediaList the media list
1314      * @param webWindow the {@link WebWindow} for some basic data
1315      * @return whether the specified {@link MediaList} is active or not
1316      */
1317     public static boolean isActive(final MediaListImpl mediaList, final WebWindow webWindow) {
1318         if (mediaList.getLength() == 0) {
1319             return true;
1320         }
1321 
1322         final int length = mediaList.getLength();
1323         for (int i = 0; i < length; i++) {
1324             final MediaQuery mediaQuery = mediaList.mediaQuery(i);
1325             boolean isActive = isActive(mediaQuery, webWindow);
1326             if (mediaQuery.isNot()) {
1327                 isActive = !isActive;
1328             }
1329             if (isActive) {
1330                 return true;
1331             }
1332         }
1333         return false;
1334     }
1335 
1336     private static boolean isActive(final MediaQuery mediaQuery, final WebWindow webWindow) {
1337         final String mediaType = mediaQuery.getMedia();
1338         if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1339             for (final Property property : mediaQuery.getProperties()) {
1340                 final double val;
1341                 switch (property.getName()) {
1342                     case "max-width":
1343                         val = pixelValue(property.getValue(), webWindow);
1344                         if (val == -1 || val < webWindow.getInnerWidth()) {
1345                             return false;
1346                         }
1347                         break;
1348 
1349                     case "min-width":
1350                         val = pixelValue(property.getValue(), webWindow);
1351                         if (val == -1 || val > webWindow.getInnerWidth()) {
1352                             return false;
1353                         }
1354                         break;
1355 
1356                     case "max-device-width":
1357                         val = pixelValue(property.getValue(), webWindow);
1358                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1359                             return false;
1360                         }
1361                         break;
1362 
1363                     case "min-device-width":
1364                         val = pixelValue(property.getValue(), webWindow);
1365                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1366                             return false;
1367                         }
1368                         break;
1369 
1370                     case "max-height":
1371                         val = pixelValue(property.getValue(), webWindow);
1372                         if (val == -1 || val < webWindow.getInnerWidth()) {
1373                             return false;
1374                         }
1375                         break;
1376 
1377                     case "min-height":
1378                         val = pixelValue(property.getValue(), webWindow);
1379                         if (val == -1 || val > webWindow.getInnerWidth()) {
1380                             return false;
1381                         }
1382                         break;
1383 
1384                     case "max-device-height":
1385                         val = pixelValue(property.getValue(), webWindow);
1386                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1387                             return false;
1388                         }
1389                         break;
1390 
1391                     case "min-device-height":
1392                         val = pixelValue(property.getValue(), webWindow);
1393                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1394                             return false;
1395                         }
1396                         break;
1397 
1398                     case "resolution":
1399                         final CSSValueImpl propValue = property.getValue();
1400                         val = resolutionValue(propValue);
1401                         if (propValue == null) {
1402                             return true;
1403                         }
1404                         if (val == -1 || Math.round(val) != webWindow.getScreen().getDeviceXDPI()) {
1405                             return false;
1406                         }
1407                         break;
1408 
1409                     case "max-resolution":
1410                         val = resolutionValue(property.getValue());
1411                         if (val == -1 || val < webWindow.getScreen().getDeviceXDPI()) {
1412                             return false;
1413                         }
1414                         break;
1415 
1416                     case "min-resolution":
1417                         val = resolutionValue(property.getValue());
1418                         if (val == -1 || val > webWindow.getScreen().getDeviceXDPI()) {
1419                             return false;
1420                         }
1421                         break;
1422 
1423                     case "orientation":
1424                         final CSSValueImpl cssValue = property.getValue();
1425                         if (cssValue == null) {
1426                             LOG.warn("CSSValue is null not supported for feature 'orientation'");
1427                             return true;
1428                         }
1429 
1430                         final String orient = cssValue.getCssText();
1431                         if ("portrait".equals(orient)) {
1432                             if (webWindow.getInnerWidth() > webWindow.getInnerHeight()) {
1433                                 return false;
1434                             }
1435                         }
1436                         else if ("landscape".equals(orient)) {
1437                             if (webWindow.getInnerWidth() < webWindow.getInnerHeight()) {
1438                                 return false;
1439                             }
1440                         }
1441                         else {
1442                             if (LOG.isWarnEnabled()) {
1443                                 LOG.warn("CSSValue '" + property.getValue().getCssText()
1444                                             + "' not supported for feature 'orientation'.");
1445                             }
1446                             return false;
1447                         }
1448                         break;
1449 
1450                     default:
1451                 }
1452             }
1453             return true;
1454         }
1455         else if ("print".equalsIgnoreCase(mediaType)) {
1456             final Page page = webWindow.getEnclosedPage();
1457             if (page instanceof SgmlPage) {
1458                 return ((SgmlPage) page).isPrinting();
1459             }
1460         }
1461         return false;
1462     }
1463 
1464     @SuppressWarnings("PMD.UselessParentheses")
1465     private static double pixelValue(final CSSValueImpl cssValue, final WebWindow webWindow) {
1466         if (cssValue == null) {
1467             LOG.warn("CSSValue is null but has to be a 'px', 'em', '%', 'ex', 'ch', "
1468                     + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1469             return -1;
1470         }
1471 
1472         final LexicalUnit.LexicalUnitType luType = cssValue.getLexicalUnitType();
1473         if (luType != null) {
1474             final int dpi;
1475 
1476             switch (luType) {
1477                 case PIXEL:
1478                     return cssValue.getDoubleValue();
1479                 case EM:
1480                     // hard coded default for the moment 16px = 1 em
1481                     return 16f * cssValue.getDoubleValue();
1482                 case PERCENTAGE:
1483                     // hard coded default for the moment 16px = 100%
1484                     return 0.16f * cssValue.getDoubleValue();
1485                 case EX:
1486                     // hard coded default for the moment 16px = 100%
1487                     return 0.16f * cssValue.getDoubleValue();
1488                 case CH:
1489                     // hard coded default for the moment 16px = 100%
1490                     return 0.16f * cssValue.getDoubleValue();
1491                 case VW:
1492                     // hard coded default for the moment 16px = 100%
1493                     return 0.16f * cssValue.getDoubleValue();
1494                 case VH:
1495                     // hard coded default for the moment 16px = 100%
1496                     return 0.16f * cssValue.getDoubleValue();
1497                 case VMIN:
1498                     // hard coded default for the moment 16px = 100%
1499                     return 0.16f * cssValue.getDoubleValue();
1500                 case VMAX:
1501                     // hard coded default for the moment 16px = 100%
1502                     return 0.16f * cssValue.getDoubleValue();
1503                 case REM:
1504                     // hard coded default for the moment 16px = 100%
1505                     return 0.16f * cssValue.getDoubleValue();
1506                 case MILLIMETER:
1507                     dpi = webWindow.getScreen().getDeviceXDPI();
1508                     return (dpi / 25.4f) * cssValue.getDoubleValue();
1509                 case QUATER:
1510                     // One quarter of a millimeter. 1Q = 1/40th of 1cm.
1511                     dpi = webWindow.getScreen().getDeviceXDPI();
1512                     return ((dpi / 25.4f) * cssValue.getDoubleValue()) / 4d;
1513                 case CENTIMETER:
1514                     dpi = webWindow.getScreen().getDeviceXDPI();
1515                     return (dpi / 254f) * cssValue.getDoubleValue();
1516                 case POINT:
1517                     dpi = webWindow.getScreen().getDeviceXDPI();
1518                     return (dpi / 72f) * cssValue.getDoubleValue();
1519                 default:
1520                     break;
1521             }
1522         }
1523         if (LOG.isWarnEnabled()) {
1524             LOG.warn("CSSValue '" + cssValue.getCssText()
1525                         + "' has to be a 'px', 'em', '%', 'ex', 'ch', "
1526                         + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1527         }
1528         return -1;
1529     }
1530 
1531     private static double resolutionValue(final CSSValueImpl cssValue) {
1532         if (cssValue == null) {
1533             LOG.warn("CSSValue is null but has to be a 'dpi', 'dpcm', or 'dppx' value.");
1534             return -1;
1535         }
1536 
1537         if (cssValue.getPrimitiveType() == CSSPrimitiveValueType.CSS_DIMENSION) {
1538             final String text = cssValue.getCssText();
1539             if (text.endsWith("dpi")) {
1540                 return cssValue.getDoubleValue();
1541             }
1542             if (text.endsWith("dpcm")) {
1543                 return 2.54f * cssValue.getDoubleValue();
1544             }
1545             if (text.endsWith("dppx")) {
1546                 return 96 * cssValue.getDoubleValue();
1547             }
1548         }
1549 
1550         if (LOG.isWarnEnabled()) {
1551             LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1552         }
1553         return -1;
1554     }
1555 
1556     /**
1557      * Modifies the specified style object by adding any style rules which apply to the specified
1558      * element.
1559      *
1560      * @param style the style to modify
1561      * @param element the element to which style rules must apply in order for them to be added to
1562      *        the specified style
1563      * @param pseudoElement a string specifying the pseudo-element to match (maybe {@code null})
1564      */
1565     public void modifyIfNecessary(final ComputedCssStyleDeclaration style, final DomElement element,
1566             final String pseudoElement) {
1567 
1568         final BrowserVersion browser = element.getPage().getWebClient().getBrowserVersion();
1569         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules =
1570                 selects(getRuleIndex(), browser, element, pseudoElement, false);
1571         for (final CSSStyleSheetImpl.SelectorEntry entry : matchingRules) {
1572             final CSSStyleDeclarationImpl dec = entry.getRule().getStyle();
1573             style.applyStyleFromSelector(dec, entry.getSelector());
1574         }
1575     }
1576 
1577     private CSSStyleSheetImpl.CSSStyleSheetRuleIndex getRuleIndex() {
1578         final CSSStyleSheetImpl styleSheet = getWrappedSheet();
1579         CSSStyleSheetImpl.CSSStyleSheetRuleIndex index = styleSheet.getRuleIndex();
1580 
1581         if (index == null) {
1582             index = new CSSStyleSheetImpl.CSSStyleSheetRuleIndex();
1583             final CSSRuleListImpl ruleList = styleSheet.getCssRules();
1584             index(index, ruleList, new HashSet<>());
1585 
1586             styleSheet.setRuleIndex(index);
1587         }
1588         return index;
1589     }
1590 
1591     private void index(final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index, final CSSRuleListImpl ruleList,
1592             final Set<String> alreadyProcessing) {
1593 
1594         for (final AbstractCSSRuleImpl rule : ruleList.getRules()) {
1595             if (rule instanceof CSSStyleRuleImpl) {
1596                 final CSSStyleRuleImpl styleRule = (CSSStyleRuleImpl) rule;
1597                 final SelectorList selectors = styleRule.getSelectors();
1598                 for (final Selector selector : selectors) {
1599                     final SimpleSelector simpleSel = selector.getSimpleSelector();
1600                     if (SelectorType.ELEMENT_NODE_SELECTOR == simpleSel.getSelectorType()) {
1601                         final ElementSelector es = (ElementSelector) simpleSel;
1602                         boolean wasClass = false;
1603                         final List<Condition> conds = es.getConditions();
1604                         if (conds != null && conds.size() == 1) {
1605                             final Condition c = conds.get(0);
1606                             if (ConditionType.CLASS_CONDITION == c.getConditionType()) {
1607                                 index.addClassSelector(es, c.getValue(), selector, styleRule);
1608                                 wasClass = true;
1609                             }
1610                         }
1611                         if (!wasClass) {
1612                             index.addElementSelector(es, selector, styleRule);
1613                         }
1614                     }
1615                     else {
1616                         index.addOtherSelector(selector, styleRule);
1617                     }
1618                 }
1619             }
1620             else if (rule instanceof CSSImportRuleImpl) {
1621                 final CSSImportRuleImpl importRule = (CSSImportRuleImpl) rule;
1622 
1623                 final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1624 
1625                 if (!alreadyProcessing.contains(sheet.getUri())) {
1626                     final CSSRuleListImpl sheetRuleList = sheet.getWrappedSheet().getCssRules();
1627                     alreadyProcessing.add(sheet.getUri());
1628 
1629                     final MediaListImpl mediaList = importRule.getMedia();
1630                     if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1631                         index(index, sheetRuleList, alreadyProcessing);
1632                     }
1633                     else {
1634                         index(index.addMedia(mediaList), sheetRuleList, alreadyProcessing);
1635                     }
1636                 }
1637             }
1638             else if (rule instanceof CSSMediaRuleImpl) {
1639                 final CSSMediaRuleImpl mediaRule = (CSSMediaRuleImpl) rule;
1640                 final MediaListImpl mediaList = mediaRule.getMediaList();
1641                 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1642                     index(index, mediaRule.getCssRules(), alreadyProcessing);
1643                 }
1644                 else {
1645                     index(index.addMedia(mediaList), mediaRule.getCssRules(), alreadyProcessing);
1646                 }
1647             }
1648         }
1649     }
1650 
1651     private List<CSSStyleSheetImpl.SelectorEntry> selects(
1652                             final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index,
1653                             final BrowserVersion browserVersion, final DomElement element,
1654                             final String pseudoElement, final boolean fromQuerySelectorAll) {
1655 
1656         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules = new ArrayList<>();
1657 
1658         if (isActive(index.getMediaList(), element.getPage().getEnclosingWindow())) {
1659             final String elementName = element.getLowercaseName();
1660             final String[] classes = StringUtils.splitAtJavaWhitespace(
1661                                                             element.getAttributeDirect("class"));
1662             final Iterator<CSSStyleSheetImpl.SelectorEntry> iter =
1663                     index.getSelectorEntriesIteratorFor(elementName, classes);
1664 
1665             CSSStyleSheetImpl.SelectorEntry entry = iter.next();
1666             while (null != entry) {
1667                 if (selects(browserVersion, entry.getSelector(),
1668                             element, pseudoElement, fromQuerySelectorAll, false)) {
1669                     matchingRules.add(entry);
1670                 }
1671                 entry = iter.next();
1672             }
1673 
1674             for (final CSSStyleSheetImpl.CSSStyleSheetRuleIndex child : index.getChildren()) {
1675                 matchingRules.addAll(selects(child, browserVersion,
1676                                                     element, pseudoElement, fromQuerySelectorAll));
1677             }
1678         }
1679 
1680         return matchingRules;
1681     }
1682 }