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      * Returns the {@link CssStyleSheet} imported by this rule.
1268      *
1269      * @param importRule the {@link CSSImportRuleImpl} that imports the {@link CssStyleSheet}
1270      * @return the {@link CssStyleSheet} imported by this rule
1271      */
1272     public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
1273         CssStyleSheet sheet = imports_.get(importRule);
1274         if (sheet == null) {
1275             final String href = importRule.getHref();
1276             final String url = UrlUtils.resolveUrl(getUri(), href);
1277             sheet = loadStylesheet(owner_, null, url);
1278             imports_.put(importRule, sheet);
1279         }
1280         return sheet;
1281     }
1282 
1283     /**
1284      * Returns {@code true} if this stylesheet is active, based on the media types it is associated with (if any).
1285      * @return {@code true} if this stylesheet is active, based on the media types it is associated with (if any)
1286      */
1287     public boolean isActive() {
1288         final String media;
1289         if (owner_ instanceof HtmlStyle style) {
1290             media = style.getMediaAttribute();
1291         }
1292         else if (owner_ instanceof HtmlLink link) {
1293             media = link.getMediaAttribute();
1294         }
1295         else {
1296             return true;
1297         }
1298 
1299         if (StringUtils.isBlank(media)) {
1300             return true;
1301         }
1302 
1303         final WebWindow webWindow = owner_.getPage().getEnclosingWindow();
1304         final MediaListImpl mediaList = parseMedia(media, webWindow.getWebClient());
1305         return isActive(mediaList, webWindow);
1306     }
1307 
1308     /**
1309      * Returns whether the specified {@link MediaList} is active or not.
1310      * @param mediaList the media list
1311      * @param webWindow the {@link WebWindow} for some basic data
1312      * @return whether the specified {@link MediaList} is active or not
1313      */
1314     public static boolean isActive(final MediaListImpl mediaList, final WebWindow webWindow) {
1315         if (mediaList.getLength() == 0) {
1316             return true;
1317         }
1318 
1319         final int length = mediaList.getLength();
1320         for (int i = 0; i < length; i++) {
1321             final MediaQuery mediaQuery = mediaList.mediaQuery(i);
1322             boolean isActive = isActive(mediaQuery, webWindow);
1323             if (mediaQuery.isNot()) {
1324                 isActive = !isActive;
1325             }
1326             if (isActive) {
1327                 return true;
1328             }
1329         }
1330         return false;
1331     }
1332 
1333     private static boolean isActive(final MediaQuery mediaQuery, final WebWindow webWindow) {
1334         final String mediaType = mediaQuery.getMedia();
1335         if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1336             for (final Property property : mediaQuery.getProperties()) {
1337                 final double val;
1338                 switch (property.getName()) {
1339                     case "max-width":
1340                         val = pixelValue(property.getValue(), webWindow);
1341                         if (val == -1 || val < webWindow.getInnerWidth()) {
1342                             return false;
1343                         }
1344                         break;
1345 
1346                     case "min-width":
1347                         val = pixelValue(property.getValue(), webWindow);
1348                         if (val == -1 || val > webWindow.getInnerWidth()) {
1349                             return false;
1350                         }
1351                         break;
1352 
1353                     case "max-device-width":
1354                         val = pixelValue(property.getValue(), webWindow);
1355                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1356                             return false;
1357                         }
1358                         break;
1359 
1360                     case "min-device-width":
1361                         val = pixelValue(property.getValue(), webWindow);
1362                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1363                             return false;
1364                         }
1365                         break;
1366 
1367                     case "max-height":
1368                         val = pixelValue(property.getValue(), webWindow);
1369                         if (val == -1 || val < webWindow.getInnerWidth()) {
1370                             return false;
1371                         }
1372                         break;
1373 
1374                     case "min-height":
1375                         val = pixelValue(property.getValue(), webWindow);
1376                         if (val == -1 || val > webWindow.getInnerWidth()) {
1377                             return false;
1378                         }
1379                         break;
1380 
1381                     case "max-device-height":
1382                         val = pixelValue(property.getValue(), webWindow);
1383                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1384                             return false;
1385                         }
1386                         break;
1387 
1388                     case "min-device-height":
1389                         val = pixelValue(property.getValue(), webWindow);
1390                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1391                             return false;
1392                         }
1393                         break;
1394 
1395                     case "resolution":
1396                         final CSSValueImpl propValue = property.getValue();
1397                         val = resolutionValue(propValue);
1398                         if (propValue == null) {
1399                             return true;
1400                         }
1401                         if (val == -1 || Math.round(val) != webWindow.getScreen().getDeviceXDPI()) {
1402                             return false;
1403                         }
1404                         break;
1405 
1406                     case "max-resolution":
1407                         val = resolutionValue(property.getValue());
1408                         if (val == -1 || val < webWindow.getScreen().getDeviceXDPI()) {
1409                             return false;
1410                         }
1411                         break;
1412 
1413                     case "min-resolution":
1414                         val = resolutionValue(property.getValue());
1415                         if (val == -1 || val > webWindow.getScreen().getDeviceXDPI()) {
1416                             return false;
1417                         }
1418                         break;
1419 
1420                     case "orientation":
1421                         final CSSValueImpl cssValue = property.getValue();
1422                         if (cssValue == null) {
1423                             LOG.warn("CSSValue is null not supported for feature 'orientation'");
1424                             return true;
1425                         }
1426 
1427                         final String orient = cssValue.getCssText();
1428                         if ("portrait".equals(orient)) {
1429                             if (webWindow.getInnerWidth() > webWindow.getInnerHeight()) {
1430                                 return false;
1431                             }
1432                         }
1433                         else if ("landscape".equals(orient)) {
1434                             if (webWindow.getInnerWidth() < webWindow.getInnerHeight()) {
1435                                 return false;
1436                             }
1437                         }
1438                         else {
1439                             if (LOG.isWarnEnabled()) {
1440                                 LOG.warn("CSSValue '" + property.getValue().getCssText()
1441                                             + "' not supported for feature 'orientation'.");
1442                             }
1443                             return false;
1444                         }
1445                         break;
1446 
1447                     default:
1448                 }
1449             }
1450             return true;
1451         }
1452         else if ("print".equalsIgnoreCase(mediaType)) {
1453             final Page page = webWindow.getEnclosedPage();
1454             if (page instanceof SgmlPage sgmlPage) {
1455                 return sgmlPage.isPrinting();
1456             }
1457         }
1458         return false;
1459     }
1460 
1461     @SuppressWarnings("PMD.UselessParentheses")
1462     private static double pixelValue(final CSSValueImpl cssValue, final WebWindow webWindow) {
1463         if (cssValue == null) {
1464             LOG.warn("CSSValue is null but has to be a 'px', 'em', '%', 'ex', 'ch', "
1465                     + "'vw', 'vh', 'vmin', 'vmax', 'dvw', 'dvh', 'dvmin', 'dvmax', "
1466                     + "'lvw', 'lvh', 'lvmin', 'lvmax', 'svw', 'svh', 'svmin', 'svmax', "
1467                     + "'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1468             return -1;
1469         }
1470 
1471         final LexicalUnit.LexicalUnitType luType = cssValue.getLexicalUnitType();
1472         if (luType != null) {
1473             final int dpi;
1474 
1475             switch (luType) {
1476                 case PIXEL:
1477                     return cssValue.getDoubleValue();
1478                 case EM:
1479                     // hard coded default for the moment 16px = 1 em
1480                     return 16f * cssValue.getDoubleValue();
1481                 case PERCENTAGE:
1482                     // hard coded default for the moment 16px = 100%
1483                     return 0.16f * cssValue.getDoubleValue();
1484                 case EX:
1485                     // hard coded default for the moment 16px = 100%
1486                     return 0.16f * cssValue.getDoubleValue();
1487                 case CH:
1488                     // hard coded default for the moment 16px = 100%
1489                     return 0.16f * cssValue.getDoubleValue();
1490                 case VW:
1491                     // hard coded default for the moment 16px = 100%
1492                     return 0.16f * cssValue.getDoubleValue();
1493                 case VH:
1494                     // hard coded default for the moment 16px = 100%
1495                     return 0.16f * cssValue.getDoubleValue();
1496                 case VMIN:
1497                     // hard coded default for the moment 16px = 100%
1498                     return 0.16f * cssValue.getDoubleValue();
1499                 case VMAX:
1500                     // hard coded default for the moment 16px = 100%
1501                     return 0.16f * cssValue.getDoubleValue();
1502                 case DVW:
1503                     // hard coded default for the moment 16px = 100%
1504                     return 0.16f * cssValue.getDoubleValue();
1505                 case DVH:
1506                     // hard coded default for the moment 16px = 100%
1507                     return 0.16f * cssValue.getDoubleValue();
1508                 case DVMIN:
1509                     // hard coded default for the moment 16px = 100%
1510                     return 0.16f * cssValue.getDoubleValue();
1511                 case DVMAX:
1512                     // hard coded default for the moment 16px = 100%
1513                     return 0.16f * cssValue.getDoubleValue();
1514                 case LVW:
1515                     // hard coded default for the moment 16px = 100%
1516                     return 0.16f * cssValue.getDoubleValue();
1517                 case LVH:
1518                     // hard coded default for the moment 16px = 100%
1519                     return 0.16f * cssValue.getDoubleValue();
1520                 case LVMIN:
1521                     // hard coded default for the moment 16px = 100%
1522                     return 0.16f * cssValue.getDoubleValue();
1523                 case LVMAX:
1524                     // hard coded default for the moment 16px = 100%
1525                     return 0.16f * cssValue.getDoubleValue();
1526                 case SVW:
1527                     // hard coded default for the moment 16px = 100%
1528                     return 0.16f * cssValue.getDoubleValue();
1529                 case SVH:
1530                     // hard coded default for the moment 16px = 100%
1531                     return 0.16f * cssValue.getDoubleValue();
1532                 case SVMIN:
1533                     // hard coded default for the moment 16px = 100%
1534                     return 0.16f * cssValue.getDoubleValue();
1535                 case SVMAX:
1536                     // hard coded default for the moment 16px = 100%
1537                     return 0.16f * cssValue.getDoubleValue();
1538                 case REM:
1539                     // hard coded default for the moment 16px = 100%
1540                     return 0.16f * cssValue.getDoubleValue();
1541                 case MILLIMETER:
1542                     dpi = webWindow.getScreen().getDeviceXDPI();
1543                     return (dpi / 25.4f) * cssValue.getDoubleValue();
1544                 case QUATER:
1545                     // One quarter of a millimeter. 1Q = 1/40th of 1cm.
1546                     dpi = webWindow.getScreen().getDeviceXDPI();
1547                     return ((dpi / 25.4f) * cssValue.getDoubleValue()) / 4d;
1548                 case CENTIMETER:
1549                     dpi = webWindow.getScreen().getDeviceXDPI();
1550                     return (dpi / 254f) * cssValue.getDoubleValue();
1551                 case POINT:
1552                     dpi = webWindow.getScreen().getDeviceXDPI();
1553                     return (dpi / 72f) * cssValue.getDoubleValue();
1554                 default:
1555                     break;
1556             }
1557         }
1558         if (LOG.isWarnEnabled()) {
1559             LOG.warn("CSSValue '" + cssValue.getCssText()
1560                         + "' has to be a 'px', 'em', '%', 'ex', 'ch', "
1561                         + "'vw', 'vh', 'vmin', 'vmax', 'dvw', 'dvh', 'dvmin', 'dvmax', "
1562                         + "'lvw', 'lvh', 'lvmin', 'lvmax', 'svw', 'svh', 'svmin', 'svmax', "
1563                         + "'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1564         }
1565         return -1;
1566     }
1567 
1568     private static double resolutionValue(final CSSValueImpl cssValue) {
1569         if (cssValue == null) {
1570             LOG.warn("CSSValue is null but has to be a 'dpi', 'dpcm', or 'dppx' value.");
1571             return -1;
1572         }
1573 
1574         if (cssValue.getPrimitiveType() == CSSPrimitiveValueType.CSS_DIMENSION) {
1575             final String text = cssValue.getCssText();
1576             if (text.endsWith("dpi")) {
1577                 return cssValue.getDoubleValue();
1578             }
1579             if (text.endsWith("dpcm")) {
1580                 return 2.54f * cssValue.getDoubleValue();
1581             }
1582             if (text.endsWith("dppx")) {
1583                 return 96 * cssValue.getDoubleValue();
1584             }
1585         }
1586 
1587         if (LOG.isWarnEnabled()) {
1588             LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1589         }
1590         return -1;
1591     }
1592 
1593     /**
1594      * Modifies the specified style object by adding any style rules which apply to the specified
1595      * element.
1596      *
1597      * @param style the style to modify
1598      * @param element the element to which style rules must apply in order for them to be added to
1599      *        the specified style
1600      * @param pseudoElement a string specifying the pseudo-element to match (maybe {@code null})
1601      */
1602     public void modifyIfNecessary(final ComputedCssStyleDeclaration style, final DomElement element,
1603             final String pseudoElement) {
1604 
1605         final BrowserVersion browser = element.getPage().getWebClient().getBrowserVersion();
1606         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules =
1607                 selects(getRuleIndex(), browser, element, pseudoElement, false);
1608         for (final CSSStyleSheetImpl.SelectorEntry entry : matchingRules) {
1609             final CSSStyleDeclarationImpl dec = entry.getRule().getStyle();
1610             style.applyStyleFromSelector(dec, entry.getSelector());
1611         }
1612     }
1613 
1614     private CSSStyleSheetImpl.CSSStyleSheetRuleIndex getRuleIndex() {
1615         final CSSStyleSheetImpl styleSheet = getWrappedSheet();
1616         CSSStyleSheetImpl.CSSStyleSheetRuleIndex index = styleSheet.getRuleIndex();
1617 
1618         if (index == null) {
1619             index = new CSSStyleSheetImpl.CSSStyleSheetRuleIndex();
1620             final CSSRuleListImpl ruleList = styleSheet.getCssRules();
1621             index(index, ruleList, new HashSet<>());
1622 
1623             styleSheet.setRuleIndex(index);
1624         }
1625         return index;
1626     }
1627 
1628     private void index(final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index, final CSSRuleListImpl ruleList,
1629             final Set<String> alreadyProcessing) {
1630 
1631         for (final AbstractCSSRuleImpl rule : ruleList.getRules()) {
1632             if (rule instanceof CSSStyleRuleImpl styleRule) {
1633                 final SelectorList selectors = styleRule.getSelectors();
1634                 for (final Selector selector : selectors) {
1635                     final SimpleSelector simpleSel = selector.getSimpleSelector();
1636                     if (SelectorType.ELEMENT_NODE_SELECTOR == simpleSel.getSelectorType()) {
1637                         final ElementSelector es = (ElementSelector) simpleSel;
1638                         boolean wasClass = false;
1639                         final List<Condition> conds = es.getConditions();
1640                         if (conds != null && conds.size() == 1) {
1641                             final Condition c = conds.get(0);
1642                             if (ConditionType.CLASS_CONDITION == c.getConditionType()) {
1643                                 index.addClassSelector(es, c.getValue(), selector, styleRule);
1644                                 wasClass = true;
1645                             }
1646                         }
1647                         if (!wasClass) {
1648                             index.addElementSelector(es, selector, styleRule);
1649                         }
1650                     }
1651                     else {
1652                         index.addOtherSelector(selector, styleRule);
1653                     }
1654                 }
1655             }
1656             else if (rule instanceof CSSImportRuleImpl importRule) {
1657 
1658                 final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1659 
1660                 if (!alreadyProcessing.contains(sheet.getUri())) {
1661                     final CSSRuleListImpl sheetRuleList = sheet.getWrappedSheet().getCssRules();
1662                     alreadyProcessing.add(sheet.getUri());
1663 
1664                     final MediaListImpl mediaList = importRule.getMedia();
1665                     if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1666                         index(index, sheetRuleList, alreadyProcessing);
1667                     }
1668                     else {
1669                         index(index.addMedia(mediaList), sheetRuleList, alreadyProcessing);
1670                     }
1671                 }
1672             }
1673             else if (rule instanceof CSSMediaRuleImpl mediaRule) {
1674                 final MediaListImpl mediaList = mediaRule.getMediaList();
1675                 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1676                     index(index, mediaRule.getCssRules(), alreadyProcessing);
1677                 }
1678                 else {
1679                     index(index.addMedia(mediaList), mediaRule.getCssRules(), alreadyProcessing);
1680                 }
1681             }
1682         }
1683     }
1684 
1685     private List<CSSStyleSheetImpl.SelectorEntry> selects(
1686                             final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index,
1687                             final BrowserVersion browserVersion, final DomElement element,
1688                             final String pseudoElement, final boolean fromQuerySelectorAll) {
1689 
1690         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules = new ArrayList<>();
1691 
1692         if (isActive(index.getMediaList(), element.getPage().getEnclosingWindow())) {
1693             final String elementName = element.getLowercaseName();
1694             final String[] classes = StringUtils.splitAtJavaWhitespace(
1695                                                             element.getAttributeDirect("class"));
1696             final Iterator<CSSStyleSheetImpl.SelectorEntry> iter =
1697                     index.getSelectorEntriesIteratorFor(elementName, classes);
1698 
1699             CSSStyleSheetImpl.SelectorEntry entry = iter.next();
1700             while (null != entry) {
1701                 if (selects(browserVersion, entry.getSelector(),
1702                             element, pseudoElement, fromQuerySelectorAll, false)) {
1703                     matchingRules.add(entry);
1704                 }
1705                 entry = iter.next();
1706             }
1707 
1708             for (final CSSStyleSheetImpl.CSSStyleSheetRuleIndex child : index.getChildren()) {
1709                 matchingRules.addAll(selects(child, browserVersion,
1710                                                     element, pseudoElement, fromQuerySelectorAll));
1711             }
1712         }
1713 
1714         return matchingRules;
1715     }
1716 }