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