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