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