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