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