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                 if (client.getBrowserVersion().hasFeature(HTMLLINK_CHECK_TYPE_FOR_STYLESHEET)) {
316                     final String type = link.getTypeAttribute();
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);
334             }
335 
336             // now we can look into the cache with the fixed request for
337             // a cached style sheet
338             final Cache cache = client.getCache();
339             final Object fromCache = cache.getCachedObject(request);
340             if (fromCache instanceof CSSStyleSheetImpl) {
341                 uri = request.getUrl().toExternalForm();
342                 return new CssStyleSheet(element, (CSSStyleSheetImpl) fromCache, uri);
343             }
344 
345             uri = response.getWebRequest().getUrl().toExternalForm();
346             client.printContentIfNecessary(response);
347             client.throwFailingHttpStatusCodeExceptionIfNecessary(response);
348             // CSS content must have downloaded OK; go ahead and build the corresponding stylesheet.
349 
350             final CssStyleSheet sheet;
351             final String contentType = response.getContentType();
352             if (StringUtils.isEmpty(contentType) || MimeType.TEXT_CSS.equals(contentType)) {
353                 try (InputStream in = response.getContentAsStreamWithBomIfApplicable()) {
354                     if (in == null) {
355                         if (LOG.isWarnEnabled()) {
356                             LOG.warn("Loading stylesheet for url '" + uri + "' returns empty responseData");
357                         }
358                         return new CssStyleSheet(element, "", uri);
359                     }
360 
361                     final Charset cssEncoding2 = response.getContentCharset();
362                     try (InputSource source = new InputSource(new InputStreamReader(in, cssEncoding2))) {
363                         source.setURI(uri);
364                         sheet = new CssStyleSheet(element, source, uri);
365                     }
366                 }
367             }
368             else {
369                 sheet = new CssStyleSheet(element, "", uri);
370             }
371 
372             // cache the style sheet
373             if (!cache.cacheIfPossible(request, response, sheet.getWrappedSheet())) {
374                 response.cleanUp();
375             }
376 
377             return sheet;
378         }
379         catch (final FailingHttpStatusCodeException e) {
380             // Got a 404 response or something like that; behave nicely.
381             if (LOG.isErrorEnabled()) {
382                 LOG.error("Exception loading " + uri, e);
383             }
384             return new CssStyleSheet(element, "", uri);
385         }
386         catch (final IOException e) {
387             // Got a basic IO error; behave nicely.
388             if (LOG.isErrorEnabled()) {
389                 LOG.error("IOException loading " + uri, e);
390             }
391             return new CssStyleSheet(element, "", uri);
392         }
393     }
394 
395     /**
396      * Returns {@code true} if the specified selector selects the specified element.
397      *
398      * @param browserVersion the browser version
399      * @param selector the selector to test
400      * @param element the element to test
401      * @param pseudoElement the pseudo element to match, (can be {@code null})
402      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)}
403      * @param throwOnSyntax throw exception if the selector syntax is incorrect
404      * @return {@code true} if it does apply, {@code false} if it doesn't apply
405      */
406     public static boolean selects(final BrowserVersion browserVersion, final Selector selector,
407             final DomElement element, final String pseudoElement, final boolean fromQuerySelectorAll,
408             final boolean throwOnSyntax) {
409         switch (selector.getSelectorType()) {
410             case ELEMENT_NODE_SELECTOR:
411                 final ElementSelector es = (ElementSelector) selector;
412 
413                 final String name;
414                 final String elementName;
415                 if (element.getPage().hasCaseSensitiveTagNames()) {
416                     name = es.getLocalName();
417                     elementName = element.getLocalName();
418                 }
419                 else {
420                     name = es.getLocalNameLowerCase();
421                     elementName = element.getLowercaseName();
422                 }
423 
424                 if (name == null || name.equals(elementName)) {
425                     final List<Condition> conditions = es.getConditions();
426                     if (conditions != null) {
427                         for (final Condition condition : conditions) {
428                             if (!selects(browserVersion, condition, element, fromQuerySelectorAll, throwOnSyntax)) {
429                                 return false;
430                             }
431                         }
432                     }
433                     return true;
434                 }
435 
436                 return false;
437 
438             case CHILD_SELECTOR:
439                 final DomNode parentNode = element.getParentNode();
440                 if (parentNode == element.getPage()) {
441                     return false;
442                 }
443                 if (!(parentNode instanceof DomElement)) {
444                     return false; // for instance parent is a DocumentFragment
445                 }
446                 final ChildSelector cs = (ChildSelector) selector;
447                 return selects(browserVersion, cs.getSimpleSelector(), element, pseudoElement,
448                             fromQuerySelectorAll, throwOnSyntax)
449                     && selects(browserVersion, cs.getAncestorSelector(), (DomElement) parentNode,
450                             pseudoElement, fromQuerySelectorAll, throwOnSyntax);
451 
452             case DESCENDANT_SELECTOR:
453                 final DescendantSelector ds = (DescendantSelector) selector;
454                 final SimpleSelector simpleSelector = ds.getSimpleSelector();
455                 if (selects(browserVersion, simpleSelector, element, pseudoElement,
456                             fromQuerySelectorAll, throwOnSyntax)) {
457                     DomNode ancestor = element;
458                     if (simpleSelector.getSelectorType() != SelectorType.PSEUDO_ELEMENT_SELECTOR) {
459                         ancestor = ancestor.getParentNode();
460                     }
461                     final Selector dsAncestorSelector = ds.getAncestorSelector();
462                     while (ancestor instanceof DomElement) {
463                         if (selects(browserVersion, dsAncestorSelector, (DomElement) ancestor, pseudoElement,
464                                 fromQuerySelectorAll, throwOnSyntax)) {
465                             return true;
466                         }
467                         ancestor = ancestor.getParentNode();
468                     }
469                 }
470                 return false;
471 
472             case DIRECT_ADJACENT_SELECTOR:
473                 final DirectAdjacentSelector das = (DirectAdjacentSelector) selector;
474                 if (selects(browserVersion, das.getSimpleSelector(), element, pseudoElement,
475                             fromQuerySelectorAll, throwOnSyntax)) {
476                     DomNode prev = element.getPreviousSibling();
477                     while (prev != null && !(prev instanceof DomElement)) {
478                         prev = prev.getPreviousSibling();
479                     }
480                     return prev != null
481                             && selects(browserVersion, das.getSelector(),
482                                     (DomElement) prev, pseudoElement, fromQuerySelectorAll, throwOnSyntax);
483                 }
484                 return false;
485 
486             case GENERAL_ADJACENT_SELECTOR:
487                 final GeneralAdjacentSelector gas = (GeneralAdjacentSelector) selector;
488                 if (selects(browserVersion, gas.getSimpleSelector(), element, pseudoElement,
489                             fromQuerySelectorAll, throwOnSyntax)) {
490                     for (DomNode prev1 = element.getPreviousSibling(); prev1 != null;
491                                                         prev1 = prev1.getPreviousSibling()) {
492                         if (prev1 instanceof DomElement
493                             && selects(browserVersion, gas.getSelector(), (DomElement) prev1,
494                                     pseudoElement, fromQuerySelectorAll, throwOnSyntax)) {
495                             return true;
496                         }
497                     }
498                 }
499                 return false;
500             case PSEUDO_ELEMENT_SELECTOR:
501                 if (pseudoElement != null && pseudoElement.length() != 0 && pseudoElement.charAt(0) == ':') {
502                     final String pseudoName = ((PseudoElementSelector) selector).getLocalName();
503                     return pseudoName.equals(pseudoElement.substring(1));
504                 }
505                 return false;
506 
507             default:
508                 if (LOG.isErrorEnabled()) {
509                     LOG.error("Unknown CSS selector type '" + selector.getSelectorType() + "'.");
510                 }
511                 return false;
512         }
513     }
514 
515     /**
516      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
517      * Returns {@code true} if the specified condition selects the specified element.
518      *
519      * @param browserVersion the browser version
520      * @param condition the condition to test
521      * @param element the element to test
522      * @param fromQuerySelectorAll whether this is called from {@link DomNode#querySelectorAll(String)}
523      * @param throwOnSyntax throw exception if the selector syntax is incorrect
524      * @return {@code true} if it does apply, {@code false} if it doesn't apply
525      */
526     static boolean selects(final BrowserVersion browserVersion,
527             final Condition condition, final DomElement element,
528             final boolean fromQuerySelectorAll, final boolean throwOnSyntax) {
529 
530         switch (condition.getConditionType()) {
531             case ID_CONDITION:
532                 return condition.getValue().equals(element.getId());
533 
534             case CLASS_CONDITION:
535                 String v3 = condition.getValue();
536                 if (v3.indexOf('\\') > -1) {
537                     v3 = UNESCAPE_SELECTOR.matcher(v3).replaceAll("$1");
538                 }
539                 final String a3 = element.getAttributeDirect("class");
540                 return selectsWhitespaceSeparated(v3, a3);
541 
542             case ATTRIBUTE_CONDITION:
543                 final AttributeCondition attributeCondition = (AttributeCondition) condition;
544                 String value = attributeCondition.getValue();
545                 if (value != null) {
546                     if (value.indexOf('\\') > -1) {
547                         value = UNESCAPE_SELECTOR.matcher(value).replaceAll("$1");
548                     }
549                     final String name = attributeCondition.getLocalName();
550                     final String attrValue = element.getAttribute(name);
551                     if (attributeCondition.isCaseInSensitive() || DomElement.TYPE_ATTRIBUTE.equals(name)) {
552                         return ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equalsIgnoreCase(value);
553                     }
554                     return ATTRIBUTE_NOT_DEFINED != attrValue && attrValue.equals(value);
555                 }
556                 return element.hasAttribute(condition.getLocalName());
557 
558             case PREFIX_ATTRIBUTE_CONDITION:
559                 final AttributeCondition prefixAttributeCondition = (AttributeCondition) condition;
560                 final String prefixValue = prefixAttributeCondition.getValue();
561                 if (prefixAttributeCondition.isCaseInSensitive()) {
562                     return !org.htmlunit.util.StringUtils.isEmptyString(prefixValue)
563                             && StringUtils.startsWithIgnoreCase(
564                                     element.getAttribute(prefixAttributeCondition.getLocalName()), prefixValue);
565                 }
566                 return !org.htmlunit.util.StringUtils.isEmptyString(prefixValue)
567                         && element.getAttribute(prefixAttributeCondition.getLocalName()).startsWith(prefixValue);
568 
569             case SUFFIX_ATTRIBUTE_CONDITION:
570                 final AttributeCondition suffixAttributeCondition = (AttributeCondition) condition;
571                 final String suffixValue = suffixAttributeCondition.getValue();
572                 if (suffixAttributeCondition.isCaseInSensitive()) {
573                     return !org.htmlunit.util.StringUtils.isEmptyString(suffixValue)
574                             && StringUtils.endsWithIgnoreCase(
575                                     element.getAttribute(suffixAttributeCondition.getLocalName()), suffixValue);
576                 }
577                 return !org.htmlunit.util.StringUtils.isEmptyString(suffixValue)
578                         && element.getAttribute(suffixAttributeCondition.getLocalName()).endsWith(suffixValue);
579 
580             case SUBSTRING_ATTRIBUTE_CONDITION:
581                 final AttributeCondition substringAttributeCondition = (AttributeCondition) condition;
582                 final String substringValue = substringAttributeCondition.getValue();
583                 if (substringAttributeCondition.isCaseInSensitive()) {
584                     return !org.htmlunit.util.StringUtils.isEmptyString(substringValue)
585                             && StringUtils.containsIgnoreCase(
586                                     element.getAttribute(substringAttributeCondition.getLocalName()), substringValue);
587                 }
588                 return !org.htmlunit.util.StringUtils.isEmptyString(substringValue)
589                         && element.getAttribute(substringAttributeCondition.getLocalName()).contains(substringValue);
590 
591             case BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
592                 final AttributeCondition beginHyphenAttributeCondition = (AttributeCondition) condition;
593                 final String v = beginHyphenAttributeCondition.getValue();
594                 final String a = element.getAttribute(beginHyphenAttributeCondition.getLocalName());
595                 if (beginHyphenAttributeCondition.isCaseInSensitive()) {
596                     return selectsHyphenSeparated(
597                             org.htmlunit.util.StringUtils.toRootLowerCase(v),
598                             org.htmlunit.util.StringUtils.toRootLowerCase(a));
599                 }
600                 return selectsHyphenSeparated(v, a);
601 
602             case ONE_OF_ATTRIBUTE_CONDITION:
603                 final AttributeCondition oneOfAttributeCondition = (AttributeCondition) condition;
604                 final String v2 = oneOfAttributeCondition.getValue();
605                 final String a2 = element.getAttribute(oneOfAttributeCondition.getLocalName());
606                 if (oneOfAttributeCondition.isCaseInSensitive()) {
607                     return selectsOneOf(
608                             org.htmlunit.util.StringUtils.toRootLowerCase(v2),
609                             org.htmlunit.util.StringUtils.toRootLowerCase(a2));
610                 }
611                 return selectsOneOf(v2, a2);
612 
613             case LANG_CONDITION:
614                 final String lcLang = condition.getValue();
615                 final int lcLangLength = lcLang.length();
616                 for (DomNode node = element; node instanceof HtmlElement; node = node.getParentNode()) {
617                     final String nodeLang = ((HtmlElement) node).getAttributeDirect("lang");
618                     if (ATTRIBUTE_NOT_DEFINED != nodeLang) {
619                         // "en", "en-GB" should be matched by "en" but not "english"
620                         return nodeLang.startsWith(lcLang)
621                             && (nodeLang.length() == lcLangLength || '-' == nodeLang.charAt(lcLangLength));
622                     }
623                 }
624                 return false;
625 
626             case NOT_PSEUDO_CLASS_CONDITION:
627                 final NotPseudoClassCondition notPseudoCondition = (NotPseudoClassCondition) condition;
628                 final SelectorList selectorList = notPseudoCondition.getSelectors();
629                 for (final Selector selector : selectorList) {
630                     if (selects(browserVersion, selector, element, null, fromQuerySelectorAll, throwOnSyntax)) {
631                         return false;
632                     }
633                 }
634                 return true;
635 
636             case PSEUDO_CLASS_CONDITION:
637                 return selectsPseudoClass(browserVersion, condition, element);
638 
639             default:
640                 if (LOG.isErrorEnabled()) {
641                     LOG.error("Unknown CSS condition type '" + condition.getConditionType() + "'.");
642                 }
643                 return false;
644         }
645     }
646 
647     private static boolean selectsOneOf(final String condition, final String attribute) {
648         // attribute.equals(condition)
649         // || attribute.startsWith(condition + " ") || attriubte.endsWith(" " + condition)
650         // || attribute.contains(" " + condition + " ");
651 
652         final int conditionLength = condition.length();
653         if (conditionLength < 1) {
654             return false;
655         }
656 
657         final int attribLength = attribute.length();
658         if (attribLength < conditionLength) {
659             return false;
660         }
661         if (attribLength > conditionLength) {
662             if (' ' == attribute.charAt(conditionLength)
663                     && attribute.startsWith(condition)) {
664                 return true;
665             }
666             if (' ' == attribute.charAt(attribLength - conditionLength - 1)
667                     && attribute.endsWith(condition)) {
668                 return true;
669             }
670             if (attribLength + 1 > conditionLength) {
671                 final StringBuilder tmp = new StringBuilder(conditionLength + 2);
672                 tmp.append(' ').append(condition).append(' ');
673                 return attribute.contains(tmp);
674             }
675             return false;
676         }
677         return attribute.equals(condition);
678     }
679 
680     private static boolean selectsHyphenSeparated(final String condition, final String attribute) {
681         final int conditionLength = condition.length();
682         if (conditionLength < 1) {
683             if (attribute != ATTRIBUTE_NOT_DEFINED) {
684                 final int attribLength = attribute.length();
685                 return attribLength == 0 || '-' == attribute.charAt(0);
686             }
687             return false;
688         }
689 
690         final int attribLength = attribute.length();
691         if (attribLength < conditionLength) {
692             return false;
693         }
694         if (attribLength > conditionLength) {
695             return '-' == attribute.charAt(conditionLength)
696                     && attribute.startsWith(condition);
697         }
698         return attribute.equals(condition);
699     }
700 
701     private static boolean selectsWhitespaceSeparated(final String condition, final String attribute) {
702         final int conditionLength = condition.length();
703         if (conditionLength < 1) {
704             return false;
705         }
706 
707         final int attribLength = attribute.length();
708         if (attribLength < conditionLength) {
709             return false;
710         }
711 
712         int pos = attribute.indexOf(condition);
713         while (pos != -1) {
714             if (pos > 0 && !Character.isWhitespace(attribute.charAt(pos - 1))) {
715                 pos = attribute.indexOf(condition, pos + 1);
716             }
717             else {
718                 final int lastPos = pos + condition.length();
719                 if (lastPos >= attribLength || Character.isWhitespace(attribute.charAt(lastPos))) {
720                     return true;
721                 }
722                 pos = attribute.indexOf(condition, pos + 1);
723             }
724         }
725 
726         return false;
727     }
728 
729     @SuppressWarnings("PMD.UselessParentheses")
730     private static boolean selectsPseudoClass(final BrowserVersion browserVersion,
731             final Condition condition, final DomElement element) {
732         final String value = condition.getValue();
733         switch (value) {
734             case "root":
735                 return element == element.getPage().getDocumentElement();
736 
737             case "enabled":
738                 return element instanceof DisabledElement && !((DisabledElement) element).isDisabled();
739 
740             case "disabled":
741                 return element instanceof DisabledElement && ((DisabledElement) element).isDisabled();
742 
743             case "focus":
744                 final HtmlPage htmlPage = element.getHtmlPageOrNull();
745                 if (htmlPage != null) {
746                     final DomElement focus = htmlPage.getFocusedElement();
747                     return element == focus;
748                 }
749                 return false;
750 
751             case "focus-within":
752                 final HtmlPage htmlPage2 = element.getHtmlPageOrNull();
753                 if (htmlPage2 != null) {
754                     final DomElement focus = htmlPage2.getFocusedElement();
755                     return element == focus || element.isAncestorOf(focus);
756                 }
757                 return false;
758 
759             case "focus-visible":
760                 final HtmlPage htmlPage3 = element.getHtmlPageOrNull();
761                 if (htmlPage3 != null) {
762                     final DomElement focus = htmlPage3.getFocusedElement();
763                     return element == focus
764                             && ((element instanceof HtmlInput && !((HtmlInput) element).isReadOnly())
765                                 || (element instanceof HtmlTextArea && !((HtmlTextArea) element).isReadOnly()));
766                 }
767                 return false;
768 
769             case "checked":
770                 return (element instanceof HtmlCheckBoxInput && ((HtmlCheckBoxInput) element).isChecked())
771                         || (element instanceof HtmlRadioButtonInput && ((HtmlRadioButtonInput) element).isChecked()
772                                 || (element instanceof HtmlOption && ((HtmlOption) element).isSelected()));
773 
774             case "required":
775                 return element instanceof HtmlElement && ((HtmlElement) element).isRequired();
776 
777             case "optional":
778                 return element instanceof HtmlElement && ((HtmlElement) element).isOptional();
779 
780             case "first-child":
781                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
782                     if (n instanceof DomElement) {
783                         return false;
784                     }
785                 }
786                 return true;
787 
788             case "last-child":
789                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
790                     if (n instanceof DomElement) {
791                         return false;
792                     }
793                 }
794                 return true;
795 
796             case "first-of-type":
797                 final String firstType = element.getNodeName();
798                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
799                     if (n instanceof DomElement && n.getNodeName().equals(firstType)) {
800                         return false;
801                     }
802                 }
803                 return true;
804 
805             case "last-of-type":
806                 final String lastType = element.getNodeName();
807                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
808                     if (n instanceof DomElement && n.getNodeName().equals(lastType)) {
809                         return false;
810                     }
811                 }
812                 return true;
813 
814             case "only-child":
815                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
816                     if (n instanceof DomElement) {
817                         return false;
818                     }
819                 }
820                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
821                     if (n instanceof DomElement) {
822                         return false;
823                     }
824                 }
825                 return true;
826 
827             case "only-of-type":
828                 final String type = element.getNodeName();
829                 for (DomNode n = element.getPreviousSibling(); n != null; n = n.getPreviousSibling()) {
830                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
831                         return false;
832                     }
833                 }
834                 for (DomNode n = element.getNextSibling(); n != null; n = n.getNextSibling()) {
835                     if (n instanceof DomElement && n.getNodeName().equals(type)) {
836                         return false;
837                     }
838                 }
839                 return true;
840 
841             case "valid":
842                 if (element instanceof HtmlForm || element instanceof ValidatableElement) {
843                     return ((HtmlElement) element).isValid();
844                 }
845                 return false;
846 
847             case "invalid":
848                 if (element instanceof HtmlForm || element instanceof ValidatableElement) {
849                     return !((HtmlElement) element).isValid();
850                 }
851                 return false;
852 
853             case "empty":
854                 return isEmpty(element);
855 
856             case "target":
857                 final String ref = element.getPage().getUrl().getRef();
858                 return StringUtils.isNotBlank(ref) && ref.equals(element.getId());
859 
860             case "hover":
861                 return element.isMouseOver();
862 
863             case "placeholder-shown":
864                 return element instanceof HtmlInput
865                         && StringUtils.isEmpty(((HtmlInput) element).getValue())
866                         && StringUtils.isNotEmpty(((HtmlInput) element).getPlaceholder());
867 
868             default:
869                 if (value.startsWith("nth-child(")) {
870                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
871                     int index = 0;
872                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
873                         if (n instanceof DomElement) {
874                             index++;
875                         }
876                     }
877                     return getNthElement(nth, index);
878                 }
879                 else if (value.startsWith("nth-last-child(")) {
880                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
881                     int index = 0;
882                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
883                         if (n instanceof DomElement) {
884                             index++;
885                         }
886                     }
887                     return getNthElement(nth, index);
888                 }
889                 else if (value.startsWith("nth-of-type(")) {
890                     final String nthType = element.getNodeName();
891                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
892                     int index = 0;
893                     for (DomNode n = element; n != null; n = n.getPreviousSibling()) {
894                         if (n instanceof DomElement && n.getNodeName().equals(nthType)) {
895                             index++;
896                         }
897                     }
898                     return getNthElement(nth, index);
899                 }
900                 else if (value.startsWith("nth-last-of-type(")) {
901                     final String nthLastType = element.getNodeName();
902                     final String nth = value.substring(value.indexOf('(') + 1, value.length() - 1);
903                     int index = 0;
904                     for (DomNode n = element; n != null; n = n.getNextSibling()) {
905                         if (n instanceof DomElement && n.getNodeName().equals(nthLastType)) {
906                             index++;
907                         }
908                     }
909                     return getNthElement(nth, index);
910                 }
911                 return false;
912         }
913     }
914 
915     private static boolean isEmpty(final DomElement element) {
916         for (DomNode n = element.getFirstChild(); n != null; n = n.getNextSibling()) {
917             if (n instanceof DomElement || n instanceof DomText) {
918                 return false;
919             }
920         }
921         return true;
922     }
923 
924     private static boolean getNthElement(final String nth, final int index) {
925         if ("odd".equalsIgnoreCase(nth)) {
926             return index % 2 != 0;
927         }
928 
929         if ("even".equalsIgnoreCase(nth)) {
930             return index % 2 == 0;
931         }
932 
933         // (numerator) * n + (denominator)
934         final int nIndex = nth.indexOf('n');
935         int denominator = 0;
936         if (nIndex != -1) {
937             String value = nth.substring(0, nIndex).trim();
938             if (org.htmlunit.util.StringUtils.equalsChar('-', value)) {
939                 denominator = -1;
940             }
941             else {
942                 if (value.length() > 0 && value.charAt(0) == '+') {
943                     value = value.substring(1);
944                 }
945                 denominator = NumberUtils.toInt(value, 1);
946             }
947         }
948 
949         String value = nth.substring(nIndex + 1).trim();
950         if (value.length() > 0 && value.charAt(0) == '+') {
951             value = value.substring(1);
952         }
953         final int numerator = NumberUtils.toInt(value, 0);
954         if (denominator == 0) {
955             return index == numerator && numerator > 0;
956         }
957 
958         final double n = (index - numerator) / (double) denominator;
959         return n >= 0 && n % 1 == 0;
960     }
961 
962     /**
963      * Parses the CSS at the specified input source. If anything at all goes wrong, this method
964      * returns an empty stylesheet.
965      *
966      * @param source the source from which to retrieve the CSS to be parsed
967      * @param client the client
968      * @return the stylesheet parsed from the specified input source
969      */
970     private static CSSStyleSheetImpl parseCSS(final InputSource source, final WebClient client) {
971         CSSStyleSheetImpl ss;
972 
973         // use a pooled parser, if any available to avoid expensive recreation
974         try (PooledCSS3Parser pooledParser = client.getCSS3Parser()) {
975             final CSSErrorHandler errorHandler = client.getCssErrorHandler();
976             final CSSOMParser parser = new CSSOMParser(pooledParser);
977             parser.setErrorHandler(errorHandler);
978             ss = parser.parseStyleSheet(source, null);
979         }
980         catch (final Throwable ex) {
981             if (LOG.isErrorEnabled()) {
982                 LOG.error("Error parsing CSS from '" + toString(source) + "': " + ex.getMessage(), ex);
983             }
984             ss = new CSSStyleSheetImpl();
985         }
986         return ss;
987     }
988 
989     /**
990      * Parses the given media string. If anything at all goes wrong, this
991      * method returns an empty MediaList list.
992      *
993      * @param mediaString the source from which to retrieve the media to be parsed
994      * @param webClient the {@link WebClient} to be used
995      * @return the media parsed from the specified input source
996      */
997     public static MediaListImpl parseMedia(final String mediaString, final WebClient webClient) {
998         MediaListImpl media = MEDIA.get(mediaString);
999         if (media != null) {
1000             return media;
1001         }
1002 
1003         // get us a pooled parser for efficiency because a new parser is expensive
1004         try (PooledCSS3Parser pooledParser = webClient.getCSS3Parser()) {
1005             final CSSOMParser parser = new CSSOMParser(pooledParser);
1006             parser.setErrorHandler(webClient.getCssErrorHandler());
1007 
1008             media = new MediaListImpl(parser.parseMedia(mediaString));
1009             MEDIA.put(mediaString, media);
1010             return media;
1011         }
1012         catch (final Exception e) {
1013             if (LOG.isErrorEnabled()) {
1014                 LOG.error("Error parsing CSS media from '" + mediaString + "': " + e.getMessage(), e);
1015             }
1016         }
1017 
1018         media = new MediaListImpl(null);
1019         MEDIA.put(mediaString, media);
1020         return media;
1021     }
1022 
1023     /**
1024      * Returns the contents of the specified input source, ignoring any {@link IOException}s.
1025      * @param source the input source from which to read
1026      * @return the contents of the specified input source, or an empty string if an {@link IOException} occurs
1027      */
1028     private static String toString(final InputSource source) {
1029         try {
1030             final Reader reader = source.getReader();
1031             if (null != reader) {
1032                 // try to reset to produce some output
1033                 if (reader instanceof StringReader) {
1034                     final StringReader sr = (StringReader) reader;
1035                     sr.reset();
1036                 }
1037                 return IOUtils.toString(reader);
1038             }
1039             return "";
1040         }
1041         catch (final IOException e) {
1042             LOG.error(e.getMessage(), e);
1043             return "";
1044         }
1045     }
1046 
1047     /**
1048      * Validates the list of selectors.
1049      * @param selectorList the selectors
1050      * @param documentMode see {@link Document#getDocumentMode()}
1051      * @param domNode the dom node the query should work on
1052      * @throws CSSException if a selector is invalid
1053      *
1054      * @deprecated as of version 4.5.0; use {@link #validateSelectors(SelectorList, DomNode)} instead
1055      */
1056     @Deprecated
1057     public static void validateSelectors(final SelectorList selectorList, final int documentMode,
1058                 final DomNode domNode) throws CSSException {
1059         for (final Selector selector : selectorList) {
1060             if (!isValidSelector(selector, domNode)) {
1061                 throw new CSSException("Invalid selector: " + selector, null);
1062             }
1063         }
1064     }
1065 
1066     /**
1067      * Validates the list of selectors.
1068      * @param selectorList the selectors
1069      * @param domNode the dom node the query should work on
1070      * @throws CSSException if a selector is invalid
1071      */
1072     public static void validateSelectors(final SelectorList selectorList, final DomNode domNode) throws CSSException {
1073         for (final Selector selector : selectorList) {
1074             if (!isValidSelector(selector, domNode)) {
1075                 throw new CSSException("Invalid selector: " + selector, null);
1076             }
1077         }
1078     }
1079 
1080     /**
1081      * @param documentMode see {@link Document#getDocumentMode()}
1082      */
1083     private static boolean isValidSelector(final Selector selector, final DomNode domNode) {
1084         switch (selector.getSelectorType()) {
1085             case ELEMENT_NODE_SELECTOR:
1086                 final List<Condition> conditions = ((ElementSelector) selector).getConditions();
1087                 if (conditions != null) {
1088                     for (final Condition condition : conditions) {
1089                         if (!isValidCondition(condition, domNode)) {
1090                             return false;
1091                         }
1092                     }
1093                 }
1094                 return true;
1095             case DESCENDANT_SELECTOR:
1096                 final DescendantSelector ds = (DescendantSelector) selector;
1097                 return isValidSelector(ds.getAncestorSelector(), domNode)
1098                         && isValidSelector(ds.getSimpleSelector(), domNode);
1099             case CHILD_SELECTOR:
1100                 final ChildSelector cs = (ChildSelector) selector;
1101                 return isValidSelector(cs.getAncestorSelector(), domNode)
1102                         && isValidSelector(cs.getSimpleSelector(), domNode);
1103             case DIRECT_ADJACENT_SELECTOR:
1104                 final DirectAdjacentSelector das = (DirectAdjacentSelector) selector;
1105                 return isValidSelector(das.getSelector(), domNode)
1106                         && isValidSelector(das.getSimpleSelector(), domNode);
1107             case GENERAL_ADJACENT_SELECTOR:
1108                 final GeneralAdjacentSelector gas = (GeneralAdjacentSelector) selector;
1109                 return isValidSelector(gas.getSelector(), domNode)
1110                         && isValidSelector(gas.getSimpleSelector(), domNode);
1111             default:
1112                 if (LOG.isWarnEnabled()) {
1113                     LOG.warn("Unhandled CSS selector type '"
1114                                 + selector.getSelectorType() + "'. Accepting it silently.");
1115                 }
1116                 return true; // at least in a first time to break less stuff
1117         }
1118     }
1119 
1120     /**
1121      * @param documentMode see {@link Document#getDocumentMode()}
1122      */
1123     private static boolean isValidCondition(final Condition condition, final DomNode domNode) {
1124         switch (condition.getConditionType()) {
1125             case ATTRIBUTE_CONDITION:
1126             case ID_CONDITION:
1127             case LANG_CONDITION:
1128             case ONE_OF_ATTRIBUTE_CONDITION:
1129             case BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
1130             case CLASS_CONDITION:
1131             case PREFIX_ATTRIBUTE_CONDITION:
1132             case SUBSTRING_ATTRIBUTE_CONDITION:
1133             case SUFFIX_ATTRIBUTE_CONDITION:
1134                 return true;
1135             case NOT_PSEUDO_CLASS_CONDITION:
1136                 final NotPseudoClassCondition notPseudoCondition = (NotPseudoClassCondition) condition;
1137                 final SelectorList selectorList = notPseudoCondition.getSelectors();
1138                 for (final Selector selector : selectorList) {
1139                     if (!isValidSelector(selector, domNode)) {
1140                         return false;
1141                     }
1142                 }
1143                 return true;
1144             case PSEUDO_CLASS_CONDITION:
1145                 String value = condition.getValue();
1146                 if (value.endsWith(")")) {
1147                     if (value.endsWith("()")) {
1148                         return false;
1149                     }
1150                     value = value.substring(0, value.indexOf('(') + 1) + ')';
1151                 }
1152 
1153                 if ("nth-child()".equals(value)) {
1154                     final String arg = StringUtils.substringBetween(condition.getValue(), "(", ")").trim();
1155                     return "even".equalsIgnoreCase(arg) || "odd".equalsIgnoreCase(arg)
1156                             || NTH_NUMERIC.matcher(arg).matches()
1157                             || NTH_COMPLEX.matcher(arg).matches();
1158                 }
1159 
1160                 if ("placeholder-shown".equals(value)) {
1161                     return true;
1162                 }
1163 
1164                 return CSS4_PSEUDO_CLASSES.contains(value);
1165             default:
1166                 if (LOG.isWarnEnabled()) {
1167                     LOG.warn("Unhandled CSS condition type '"
1168                                 + condition.getConditionType() + "'. Accepting it silently.");
1169                 }
1170                 return true;
1171         }
1172     }
1173 
1174     /**
1175      * @param importRule the {@link CSSImportRuleImpl} that imports the {@link CssStyleSheet}
1176      * @return the {@link CssStyleSheet} imported by this rule
1177      */
1178     public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
1179         CssStyleSheet sheet = imports_.get(importRule);
1180         if (sheet == null) {
1181             final String href = importRule.getHref();
1182             final String url = UrlUtils.resolveUrl(getUri(), href);
1183             sheet = loadStylesheet(owner_, null, url);
1184             imports_.put(importRule, sheet);
1185         }
1186         return sheet;
1187     }
1188 
1189     /**
1190      * Returns {@code true} if this stylesheet is active, based on the media types it is associated with (if any).
1191      * @return {@code true} if this stylesheet is active, based on the media types it is associated with (if any)
1192      */
1193     public boolean isActive() {
1194         final String media;
1195         if (owner_ instanceof HtmlStyle) {
1196             final HtmlStyle style = (HtmlStyle) owner_;
1197             media = style.getMediaAttribute();
1198         }
1199         else if (owner_ instanceof HtmlLink) {
1200             final HtmlLink link = (HtmlLink) owner_;
1201             media = link.getMediaAttribute();
1202         }
1203         else {
1204             return true;
1205         }
1206 
1207         if (StringUtils.isBlank(media)) {
1208             return true;
1209         }
1210 
1211         final WebWindow webWindow = owner_.getPage().getEnclosingWindow();
1212         final MediaListImpl mediaList = parseMedia(media, webWindow.getWebClient());
1213         return isActive(mediaList, webWindow);
1214     }
1215 
1216     /**
1217      * Returns whether the specified {@link MediaList} is active or not.
1218      * @param mediaList the media list
1219      * @param webWindow the {@link WebWindow} for some basic data
1220      * @return whether the specified {@link MediaList} is active or not
1221      */
1222     public static boolean isActive(final MediaListImpl mediaList, final WebWindow webWindow) {
1223         if (mediaList.getLength() == 0) {
1224             return true;
1225         }
1226 
1227         for (int i = 0; i < mediaList.getLength(); i++) {
1228             final MediaQuery mediaQuery = mediaList.mediaQuery(i);
1229             boolean isActive = isActive(mediaQuery, webWindow);
1230             if (mediaQuery.isNot()) {
1231                 isActive = !isActive;
1232             }
1233             if (isActive) {
1234                 return true;
1235             }
1236         }
1237         return false;
1238     }
1239 
1240     private static boolean isActive(final MediaQuery mediaQuery, final WebWindow webWindow) {
1241         final String mediaType = mediaQuery.getMedia();
1242         if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1243             for (final Property property : mediaQuery.getProperties()) {
1244                 final double val;
1245                 switch (property.getName()) {
1246                     case "max-width":
1247                         val = pixelValue(property.getValue(), webWindow);
1248                         if (val == -1 || val < webWindow.getInnerWidth()) {
1249                             return false;
1250                         }
1251                         break;
1252 
1253                     case "min-width":
1254                         val = pixelValue(property.getValue(), webWindow);
1255                         if (val == -1 || val > webWindow.getInnerWidth()) {
1256                             return false;
1257                         }
1258                         break;
1259 
1260                     case "max-device-width":
1261                         val = pixelValue(property.getValue(), webWindow);
1262                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1263                             return false;
1264                         }
1265                         break;
1266 
1267                     case "min-device-width":
1268                         val = pixelValue(property.getValue(), webWindow);
1269                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1270                             return false;
1271                         }
1272                         break;
1273 
1274                     case "max-height":
1275                         val = pixelValue(property.getValue(), webWindow);
1276                         if (val == -1 || val < webWindow.getInnerWidth()) {
1277                             return false;
1278                         }
1279                         break;
1280 
1281                     case "min-height":
1282                         val = pixelValue(property.getValue(), webWindow);
1283                         if (val == -1 || val > webWindow.getInnerWidth()) {
1284                             return false;
1285                         }
1286                         break;
1287 
1288                     case "max-device-height":
1289                         val = pixelValue(property.getValue(), webWindow);
1290                         if (val == -1 || val < webWindow.getScreen().getWidth()) {
1291                             return false;
1292                         }
1293                         break;
1294 
1295                     case "min-device-height":
1296                         val = pixelValue(property.getValue(), webWindow);
1297                         if (val == -1 || val > webWindow.getScreen().getWidth()) {
1298                             return false;
1299                         }
1300                         break;
1301 
1302                     case "resolution":
1303                         final CSSValueImpl propValue = property.getValue();
1304                         val = resolutionValue(propValue);
1305                         if (propValue == null) {
1306                             return true;
1307                         }
1308                         if (val == -1 || Math.round(val) != webWindow.getScreen().getDeviceXDPI()) {
1309                             return false;
1310                         }
1311                         break;
1312 
1313                     case "max-resolution":
1314                         val = resolutionValue(property.getValue());
1315                         if (val == -1 || val < webWindow.getScreen().getDeviceXDPI()) {
1316                             return false;
1317                         }
1318                         break;
1319 
1320                     case "min-resolution":
1321                         val = resolutionValue(property.getValue());
1322                         if (val == -1 || val > webWindow.getScreen().getDeviceXDPI()) {
1323                             return false;
1324                         }
1325                         break;
1326 
1327                     case "orientation":
1328                         final CSSValueImpl cssValue = property.getValue();
1329                         if (cssValue == null) {
1330                             LOG.warn("CSSValue is null not supported for feature 'orientation'");
1331                             return true;
1332                         }
1333 
1334                         final String orient = cssValue.getCssText();
1335                         if ("portrait".equals(orient)) {
1336                             if (webWindow.getInnerWidth() > webWindow.getInnerHeight()) {
1337                                 return false;
1338                             }
1339                         }
1340                         else if ("landscape".equals(orient)) {
1341                             if (webWindow.getInnerWidth() < webWindow.getInnerHeight()) {
1342                                 return false;
1343                             }
1344                         }
1345                         else {
1346                             if (LOG.isWarnEnabled()) {
1347                                 LOG.warn("CSSValue '" + property.getValue().getCssText()
1348                                             + "' not supported for feature 'orientation'.");
1349                             }
1350                             return false;
1351                         }
1352                         break;
1353 
1354                     default:
1355                 }
1356             }
1357             return true;
1358         }
1359         else if ("print".equalsIgnoreCase(mediaType)) {
1360             final Page page = webWindow.getEnclosedPage();
1361             if (page instanceof SgmlPage) {
1362                 return ((SgmlPage) page).isPrinting();
1363             }
1364         }
1365         return false;
1366     }
1367 
1368     @SuppressWarnings("PMD.UselessParentheses")
1369     private static double pixelValue(final CSSValueImpl cssValue, final WebWindow webWindow) {
1370         if (cssValue == null) {
1371             LOG.warn("CSSValue is null but has to be a 'px', 'em', '%', 'ex', 'ch', "
1372                     + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1373             return -1;
1374         }
1375 
1376         final LexicalUnit.LexicalUnitType luType = cssValue.getLexicalUnitType();
1377         if (luType != null) {
1378             final int dpi;
1379 
1380             switch (luType) {
1381                 case PIXEL:
1382                     return cssValue.getDoubleValue();
1383                 case EM:
1384                     // hard coded default for the moment 16px = 1 em
1385                     return 16f * cssValue.getDoubleValue();
1386                 case PERCENTAGE:
1387                     // hard coded default for the moment 16px = 100%
1388                     return 0.16f * cssValue.getDoubleValue();
1389                 case EX:
1390                     // hard coded default for the moment 16px = 100%
1391                     return 0.16f * cssValue.getDoubleValue();
1392                 case CH:
1393                     // hard coded default for the moment 16px = 100%
1394                     return 0.16f * cssValue.getDoubleValue();
1395                 case VW:
1396                     // hard coded default for the moment 16px = 100%
1397                     return 0.16f * cssValue.getDoubleValue();
1398                 case VH:
1399                     // hard coded default for the moment 16px = 100%
1400                     return 0.16f * cssValue.getDoubleValue();
1401                 case VMIN:
1402                     // hard coded default for the moment 16px = 100%
1403                     return 0.16f * cssValue.getDoubleValue();
1404                 case VMAX:
1405                     // hard coded default for the moment 16px = 100%
1406                     return 0.16f * cssValue.getDoubleValue();
1407                 case REM:
1408                     // hard coded default for the moment 16px = 100%
1409                     return 0.16f * cssValue.getDoubleValue();
1410                 case MILLIMETER:
1411                     dpi = webWindow.getScreen().getDeviceXDPI();
1412                     return (dpi / 25.4f) * cssValue.getDoubleValue();
1413                 case QUATER:
1414                     // One quarter of a millimeter. 1Q = 1/40th of 1cm.
1415                     dpi = webWindow.getScreen().getDeviceXDPI();
1416                     return ((dpi / 25.4f) * cssValue.getDoubleValue()) / 4d;
1417                 case CENTIMETER:
1418                     dpi = webWindow.getScreen().getDeviceXDPI();
1419                     return (dpi / 254f) * cssValue.getDoubleValue();
1420                 case POINT:
1421                     dpi = webWindow.getScreen().getDeviceXDPI();
1422                     return (dpi / 72f) * cssValue.getDoubleValue();
1423                 default:
1424                     break;
1425             }
1426         }
1427         if (LOG.isWarnEnabled()) {
1428             LOG.warn("CSSValue '" + cssValue.getCssText()
1429                         + "' has to be a 'px', 'em', '%', 'ex', 'ch', "
1430                         + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1431         }
1432         return -1;
1433     }
1434 
1435     private static double resolutionValue(final CSSValueImpl cssValue) {
1436         if (cssValue == null) {
1437             LOG.warn("CSSValue is null but has to be a 'dpi', 'dpcm', or 'dppx' value.");
1438             return -1;
1439         }
1440 
1441         if (cssValue.getPrimitiveType() == CSSPrimitiveValueType.CSS_DIMENSION) {
1442             final String text = cssValue.getCssText();
1443             if (text.endsWith("dpi")) {
1444                 return cssValue.getDoubleValue();
1445             }
1446             if (text.endsWith("dpcm")) {
1447                 return 2.54f * cssValue.getDoubleValue();
1448             }
1449             if (text.endsWith("dppx")) {
1450                 return 96 * cssValue.getDoubleValue();
1451             }
1452         }
1453 
1454         if (LOG.isWarnEnabled()) {
1455             LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1456         }
1457         return -1;
1458     }
1459 
1460     /**
1461      * Modifies the specified style object by adding any style rules which apply to the specified
1462      * element.
1463      *
1464      * @param style the style to modify
1465      * @param element the element to which style rules must apply in order for them to be added to
1466      *        the specified style
1467      * @param pseudoElement a string specifying the pseudo-element to match (may be {@code null})
1468      */
1469     public void modifyIfNecessary(final ComputedCssStyleDeclaration style, final DomElement element,
1470             final String pseudoElement) {
1471 
1472         final BrowserVersion browser = element.getPage().getWebClient().getBrowserVersion();
1473         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules =
1474                 selects(getRuleIndex(), browser, element, pseudoElement, false);
1475         for (final CSSStyleSheetImpl.SelectorEntry entry : matchingRules) {
1476             final CSSStyleDeclarationImpl dec = entry.getRule().getStyle();
1477             style.applyStyleFromSelector(dec, entry.getSelector());
1478         }
1479     }
1480 
1481     private CSSStyleSheetImpl.CSSStyleSheetRuleIndex getRuleIndex() {
1482         final CSSStyleSheetImpl styleSheet = getWrappedSheet();
1483         CSSStyleSheetImpl.CSSStyleSheetRuleIndex index = styleSheet.getRuleIndex();
1484 
1485         if (index == null) {
1486             index = new CSSStyleSheetImpl.CSSStyleSheetRuleIndex();
1487             final CSSRuleListImpl ruleList = styleSheet.getCssRules();
1488             index(index, ruleList, new HashSet<>());
1489 
1490             styleSheet.setRuleIndex(index);
1491         }
1492         return index;
1493     }
1494 
1495     private void index(final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index, final CSSRuleListImpl ruleList,
1496             final Set<String> alreadyProcessing) {
1497 
1498         for (final AbstractCSSRuleImpl rule : ruleList.getRules()) {
1499             if (rule instanceof CSSStyleRuleImpl) {
1500                 final CSSStyleRuleImpl styleRule = (CSSStyleRuleImpl) rule;
1501                 final SelectorList selectors = styleRule.getSelectors();
1502                 for (final Selector selector : selectors) {
1503                     final SimpleSelector simpleSel = selector.getSimpleSelector();
1504                     if (SelectorType.ELEMENT_NODE_SELECTOR == simpleSel.getSelectorType()) {
1505                         final ElementSelector es = (ElementSelector) simpleSel;
1506                         boolean wasClass = false;
1507                         final List<Condition> conds = es.getConditions();
1508                         if (conds != null && conds.size() == 1) {
1509                             final Condition c = conds.get(0);
1510                             if (ConditionType.CLASS_CONDITION == c.getConditionType()) {
1511                                 index.addClassSelector(es, c.getValue(), selector, styleRule);
1512                                 wasClass = true;
1513                             }
1514                         }
1515                         if (!wasClass) {
1516                             index.addElementSelector(es, selector, styleRule);
1517                         }
1518                     }
1519                     else {
1520                         index.addOtherSelector(selector, styleRule);
1521                     }
1522                 }
1523             }
1524             else if (rule instanceof CSSImportRuleImpl) {
1525                 final CSSImportRuleImpl importRule = (CSSImportRuleImpl) rule;
1526 
1527                 final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1528 
1529                 if (!alreadyProcessing.contains(sheet.getUri())) {
1530                     final CSSRuleListImpl sheetRuleList = sheet.getWrappedSheet().getCssRules();
1531                     alreadyProcessing.add(sheet.getUri());
1532 
1533                     final MediaListImpl mediaList = importRule.getMedia();
1534                     if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1535                         index(index, sheetRuleList, alreadyProcessing);
1536                     }
1537                     else {
1538                         index(index.addMedia(mediaList), sheetRuleList, alreadyProcessing);
1539                     }
1540                 }
1541             }
1542             else if (rule instanceof CSSMediaRuleImpl) {
1543                 final CSSMediaRuleImpl mediaRule = (CSSMediaRuleImpl) rule;
1544                 final MediaListImpl mediaList = mediaRule.getMediaList();
1545                 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1546                     index(index, mediaRule.getCssRules(), alreadyProcessing);
1547                 }
1548                 else {
1549                     index(index.addMedia(mediaList), mediaRule.getCssRules(), alreadyProcessing);
1550                 }
1551             }
1552         }
1553     }
1554 
1555     private List<CSSStyleSheetImpl.SelectorEntry> selects(
1556                             final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index,
1557                             final BrowserVersion browserVersion, final DomElement element,
1558                             final String pseudoElement, final boolean fromQuerySelectorAll) {
1559 
1560         final List<CSSStyleSheetImpl.SelectorEntry> matchingRules = new ArrayList<>();
1561 
1562         if (isActive(index.getMediaList(), element.getPage().getEnclosingWindow())) {
1563             final String elementName = element.getLowercaseName();
1564             final String[] classes = org.htmlunit.util.StringUtils.splitAtJavaWhitespace(
1565                                                             element.getAttributeDirect("class"));
1566             final Iterator<CSSStyleSheetImpl.SelectorEntry> iter =
1567                     index.getSelectorEntriesIteratorFor(elementName, classes);
1568 
1569             CSSStyleSheetImpl.SelectorEntry entry = iter.next();
1570             while (null != entry) {
1571                 if (selects(browserVersion, entry.getSelector(),
1572                             element, pseudoElement, fromQuerySelectorAll, false)) {
1573                     matchingRules.add(entry);
1574                 }
1575                 entry = iter.next();
1576             }
1577 
1578             for (final CSSStyleSheetImpl.CSSStyleSheetRuleIndex child : index.getChildren()) {
1579                 matchingRules.addAll(selects(child, browserVersion,
1580                                                     element, pseudoElement, fromQuerySelectorAll));
1581             }
1582         }
1583 
1584         return matchingRules;
1585     }
1586 }