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.math.NumberUtils;
42 import org.apache.commons.logging.Log;
43 import org.apache.commons.logging.LogFactory;
44 import org.htmlunit.BrowserVersion;
45 import org.htmlunit.Cache;
46 import org.htmlunit.FailingHttpStatusCodeException;
47 import org.htmlunit.Page;
48 import org.htmlunit.SgmlPage;
49 import org.htmlunit.WebClient;
50 import org.htmlunit.WebClient.PooledCSS3Parser;
51 import org.htmlunit.WebRequest;
52 import org.htmlunit.WebResponse;
53 import org.htmlunit.WebWindow;
54 import org.htmlunit.cssparser.dom.AbstractCSSRuleImpl;
55 import org.htmlunit.cssparser.dom.CSSImportRuleImpl;
56 import org.htmlunit.cssparser.dom.CSSMediaRuleImpl;
57 import org.htmlunit.cssparser.dom.CSSRuleListImpl;
58 import org.htmlunit.cssparser.dom.CSSStyleDeclarationImpl;
59 import org.htmlunit.cssparser.dom.CSSStyleRuleImpl;
60 import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
61 import org.htmlunit.cssparser.dom.CSSValueImpl;
62 import org.htmlunit.cssparser.dom.CSSValueImpl.CSSPrimitiveValueType;
63 import org.htmlunit.cssparser.dom.MediaListImpl;
64 import org.htmlunit.cssparser.dom.Property;
65 import org.htmlunit.cssparser.parser.CSSErrorHandler;
66 import org.htmlunit.cssparser.parser.CSSException;
67 import org.htmlunit.cssparser.parser.CSSOMParser;
68 import org.htmlunit.cssparser.parser.InputSource;
69 import org.htmlunit.cssparser.parser.LexicalUnit;
70 import org.htmlunit.cssparser.parser.condition.AttributeCondition;
71 import org.htmlunit.cssparser.parser.condition.Condition;
72 import org.htmlunit.cssparser.parser.condition.Condition.ConditionType;
73 import org.htmlunit.cssparser.parser.condition.NotPseudoClassCondition;
74 import org.htmlunit.cssparser.parser.media.MediaQuery;
75 import org.htmlunit.cssparser.parser.selector.ChildSelector;
76 import org.htmlunit.cssparser.parser.selector.DescendantSelector;
77 import org.htmlunit.cssparser.parser.selector.DirectAdjacentSelector;
78 import org.htmlunit.cssparser.parser.selector.ElementSelector;
79 import org.htmlunit.cssparser.parser.selector.GeneralAdjacentSelector;
80 import org.htmlunit.cssparser.parser.selector.PseudoElementSelector;
81 import org.htmlunit.cssparser.parser.selector.Selector;
82 import org.htmlunit.cssparser.parser.selector.Selector.SelectorType;
83 import org.htmlunit.cssparser.parser.selector.SelectorList;
84 import org.htmlunit.cssparser.parser.selector.SimpleSelector;
85 import org.htmlunit.html.DisabledElement;
86 import org.htmlunit.html.DomElement;
87 import org.htmlunit.html.DomNode;
88 import org.htmlunit.html.DomText;
89 import org.htmlunit.html.HtmlCheckBoxInput;
90 import org.htmlunit.html.HtmlElement;
91 import org.htmlunit.html.HtmlForm;
92 import org.htmlunit.html.HtmlInput;
93 import org.htmlunit.html.HtmlLink;
94 import org.htmlunit.html.HtmlOption;
95 import org.htmlunit.html.HtmlPage;
96 import org.htmlunit.html.HtmlRadioButtonInput;
97 import org.htmlunit.html.HtmlStyle;
98 import org.htmlunit.html.HtmlTextArea;
99 import org.htmlunit.html.ValidatableElement;
100 import org.htmlunit.javascript.host.css.MediaList;
101 import org.htmlunit.javascript.host.dom.Document;
102 import org.htmlunit.util.MimeType;
103 import org.htmlunit.util.StringUtils;
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.isEmptyOrNull(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 !StringUtils.isEmptyString(prefixValue)
566 && StringUtils.startsWithIgnoreCase(
567 element.getAttribute(prefixAttributeCondition.getLocalName()), prefixValue);
568 }
569 return !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 !StringUtils.isEmptyString(suffixValue)
577 && StringUtils.endsWithIgnoreCase(
578 element.getAttribute(suffixAttributeCondition.getLocalName()), suffixValue);
579 }
580 return !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 !StringUtils.isEmptyString(substringValue)
588 && StringUtils.containsIgnoreCase(
589 element.getAttribute(substringAttributeCondition.getLocalName()), substringValue);
590 }
591 return !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 StringUtils.toRootLowerCase(v),
601 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 StringUtils.toRootLowerCase(v2),
612 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.isEmptyOrNull(((HtmlInput) element).getValue())
869 && !StringUtils.isEmptyOrNull(((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 (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 = org.apache.commons.lang3.StringUtils
1139 .substringBetween(condition.getValue(), "(", ")").trim();
1140 return "even".equalsIgnoreCase(arg) || "odd".equalsIgnoreCase(arg)
1141 || NTH_NUMERIC.matcher(arg).matches()
1142 || NTH_COMPLEX.matcher(arg).matches();
1143 }
1144
1145 if ("placeholder-shown".equals(value)) {
1146 return true;
1147 }
1148
1149 return CSS4_PSEUDO_CLASSES.contains(value);
1150 default:
1151 if (LOG.isWarnEnabled()) {
1152 LOG.warn("Unhandled CSS condition type '"
1153 + condition.getConditionType() + "'. Accepting it silently.");
1154 }
1155 return true;
1156 }
1157 }
1158
1159
1160
1161
1162
1163 public CssStyleSheet getImportedStyleSheet(final CSSImportRuleImpl importRule) {
1164 CssStyleSheet sheet = imports_.get(importRule);
1165 if (sheet == null) {
1166 final String href = importRule.getHref();
1167 final String url = UrlUtils.resolveUrl(getUri(), href);
1168 sheet = loadStylesheet(owner_, null, url);
1169 imports_.put(importRule, sheet);
1170 }
1171 return sheet;
1172 }
1173
1174
1175
1176
1177
1178 public boolean isActive() {
1179 final String media;
1180 if (owner_ instanceof HtmlStyle) {
1181 final HtmlStyle style = (HtmlStyle) owner_;
1182 media = style.getMediaAttribute();
1183 }
1184 else if (owner_ instanceof HtmlLink) {
1185 final HtmlLink link = (HtmlLink) owner_;
1186 media = link.getMediaAttribute();
1187 }
1188 else {
1189 return true;
1190 }
1191
1192 if (StringUtils.isBlank(media)) {
1193 return true;
1194 }
1195
1196 final WebWindow webWindow = owner_.getPage().getEnclosingWindow();
1197 final MediaListImpl mediaList = parseMedia(media, webWindow.getWebClient());
1198 return isActive(mediaList, webWindow);
1199 }
1200
1201
1202
1203
1204
1205
1206
1207 public static boolean isActive(final MediaListImpl mediaList, final WebWindow webWindow) {
1208 if (mediaList.getLength() == 0) {
1209 return true;
1210 }
1211
1212 final int length = mediaList.getLength();
1213 for (int i = 0; i < length; i++) {
1214 final MediaQuery mediaQuery = mediaList.mediaQuery(i);
1215 boolean isActive = isActive(mediaQuery, webWindow);
1216 if (mediaQuery.isNot()) {
1217 isActive = !isActive;
1218 }
1219 if (isActive) {
1220 return true;
1221 }
1222 }
1223 return false;
1224 }
1225
1226 private static boolean isActive(final MediaQuery mediaQuery, final WebWindow webWindow) {
1227 final String mediaType = mediaQuery.getMedia();
1228 if ("screen".equalsIgnoreCase(mediaType) || "all".equalsIgnoreCase(mediaType)) {
1229 for (final Property property : mediaQuery.getProperties()) {
1230 final double val;
1231 switch (property.getName()) {
1232 case "max-width":
1233 val = pixelValue(property.getValue(), webWindow);
1234 if (val == -1 || val < webWindow.getInnerWidth()) {
1235 return false;
1236 }
1237 break;
1238
1239 case "min-width":
1240 val = pixelValue(property.getValue(), webWindow);
1241 if (val == -1 || val > webWindow.getInnerWidth()) {
1242 return false;
1243 }
1244 break;
1245
1246 case "max-device-width":
1247 val = pixelValue(property.getValue(), webWindow);
1248 if (val == -1 || val < webWindow.getScreen().getWidth()) {
1249 return false;
1250 }
1251 break;
1252
1253 case "min-device-width":
1254 val = pixelValue(property.getValue(), webWindow);
1255 if (val == -1 || val > webWindow.getScreen().getWidth()) {
1256 return false;
1257 }
1258 break;
1259
1260 case "max-height":
1261 val = pixelValue(property.getValue(), webWindow);
1262 if (val == -1 || val < webWindow.getInnerWidth()) {
1263 return false;
1264 }
1265 break;
1266
1267 case "min-height":
1268 val = pixelValue(property.getValue(), webWindow);
1269 if (val == -1 || val > webWindow.getInnerWidth()) {
1270 return false;
1271 }
1272 break;
1273
1274 case "max-device-height":
1275 val = pixelValue(property.getValue(), webWindow);
1276 if (val == -1 || val < webWindow.getScreen().getWidth()) {
1277 return false;
1278 }
1279 break;
1280
1281 case "min-device-height":
1282 val = pixelValue(property.getValue(), webWindow);
1283 if (val == -1 || val > webWindow.getScreen().getWidth()) {
1284 return false;
1285 }
1286 break;
1287
1288 case "resolution":
1289 final CSSValueImpl propValue = property.getValue();
1290 val = resolutionValue(propValue);
1291 if (propValue == null) {
1292 return true;
1293 }
1294 if (val == -1 || Math.round(val) != webWindow.getScreen().getDeviceXDPI()) {
1295 return false;
1296 }
1297 break;
1298
1299 case "max-resolution":
1300 val = resolutionValue(property.getValue());
1301 if (val == -1 || val < webWindow.getScreen().getDeviceXDPI()) {
1302 return false;
1303 }
1304 break;
1305
1306 case "min-resolution":
1307 val = resolutionValue(property.getValue());
1308 if (val == -1 || val > webWindow.getScreen().getDeviceXDPI()) {
1309 return false;
1310 }
1311 break;
1312
1313 case "orientation":
1314 final CSSValueImpl cssValue = property.getValue();
1315 if (cssValue == null) {
1316 LOG.warn("CSSValue is null not supported for feature 'orientation'");
1317 return true;
1318 }
1319
1320 final String orient = cssValue.getCssText();
1321 if ("portrait".equals(orient)) {
1322 if (webWindow.getInnerWidth() > webWindow.getInnerHeight()) {
1323 return false;
1324 }
1325 }
1326 else if ("landscape".equals(orient)) {
1327 if (webWindow.getInnerWidth() < webWindow.getInnerHeight()) {
1328 return false;
1329 }
1330 }
1331 else {
1332 if (LOG.isWarnEnabled()) {
1333 LOG.warn("CSSValue '" + property.getValue().getCssText()
1334 + "' not supported for feature 'orientation'.");
1335 }
1336 return false;
1337 }
1338 break;
1339
1340 default:
1341 }
1342 }
1343 return true;
1344 }
1345 else if ("print".equalsIgnoreCase(mediaType)) {
1346 final Page page = webWindow.getEnclosedPage();
1347 if (page instanceof SgmlPage) {
1348 return ((SgmlPage) page).isPrinting();
1349 }
1350 }
1351 return false;
1352 }
1353
1354 @SuppressWarnings("PMD.UselessParentheses")
1355 private static double pixelValue(final CSSValueImpl cssValue, final WebWindow webWindow) {
1356 if (cssValue == null) {
1357 LOG.warn("CSSValue is null but has to be a 'px', 'em', '%', 'ex', 'ch', "
1358 + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1359 return -1;
1360 }
1361
1362 final LexicalUnit.LexicalUnitType luType = cssValue.getLexicalUnitType();
1363 if (luType != null) {
1364 final int dpi;
1365
1366 switch (luType) {
1367 case PIXEL:
1368 return cssValue.getDoubleValue();
1369 case EM:
1370
1371 return 16f * cssValue.getDoubleValue();
1372 case PERCENTAGE:
1373
1374 return 0.16f * cssValue.getDoubleValue();
1375 case EX:
1376
1377 return 0.16f * cssValue.getDoubleValue();
1378 case CH:
1379
1380 return 0.16f * cssValue.getDoubleValue();
1381 case VW:
1382
1383 return 0.16f * cssValue.getDoubleValue();
1384 case VH:
1385
1386 return 0.16f * cssValue.getDoubleValue();
1387 case VMIN:
1388
1389 return 0.16f * cssValue.getDoubleValue();
1390 case VMAX:
1391
1392 return 0.16f * cssValue.getDoubleValue();
1393 case REM:
1394
1395 return 0.16f * cssValue.getDoubleValue();
1396 case MILLIMETER:
1397 dpi = webWindow.getScreen().getDeviceXDPI();
1398 return (dpi / 25.4f) * cssValue.getDoubleValue();
1399 case QUATER:
1400
1401 dpi = webWindow.getScreen().getDeviceXDPI();
1402 return ((dpi / 25.4f) * cssValue.getDoubleValue()) / 4d;
1403 case CENTIMETER:
1404 dpi = webWindow.getScreen().getDeviceXDPI();
1405 return (dpi / 254f) * cssValue.getDoubleValue();
1406 case POINT:
1407 dpi = webWindow.getScreen().getDeviceXDPI();
1408 return (dpi / 72f) * cssValue.getDoubleValue();
1409 default:
1410 break;
1411 }
1412 }
1413 if (LOG.isWarnEnabled()) {
1414 LOG.warn("CSSValue '" + cssValue.getCssText()
1415 + "' has to be a 'px', 'em', '%', 'ex', 'ch', "
1416 + "'vw', 'vh', 'vmin', 'vmax', 'rem', 'mm', 'cm', 'Q', or 'pt' value.");
1417 }
1418 return -1;
1419 }
1420
1421 private static double resolutionValue(final CSSValueImpl cssValue) {
1422 if (cssValue == null) {
1423 LOG.warn("CSSValue is null but has to be a 'dpi', 'dpcm', or 'dppx' value.");
1424 return -1;
1425 }
1426
1427 if (cssValue.getPrimitiveType() == CSSPrimitiveValueType.CSS_DIMENSION) {
1428 final String text = cssValue.getCssText();
1429 if (text.endsWith("dpi")) {
1430 return cssValue.getDoubleValue();
1431 }
1432 if (text.endsWith("dpcm")) {
1433 return 2.54f * cssValue.getDoubleValue();
1434 }
1435 if (text.endsWith("dppx")) {
1436 return 96 * cssValue.getDoubleValue();
1437 }
1438 }
1439
1440 if (LOG.isWarnEnabled()) {
1441 LOG.warn("CSSValue '" + cssValue.getCssText() + "' has to be a 'dpi', 'dpcm', or 'dppx' value.");
1442 }
1443 return -1;
1444 }
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455 public void modifyIfNecessary(final ComputedCssStyleDeclaration style, final DomElement element,
1456 final String pseudoElement) {
1457
1458 final BrowserVersion browser = element.getPage().getWebClient().getBrowserVersion();
1459 final List<CSSStyleSheetImpl.SelectorEntry> matchingRules =
1460 selects(getRuleIndex(), browser, element, pseudoElement, false);
1461 for (final CSSStyleSheetImpl.SelectorEntry entry : matchingRules) {
1462 final CSSStyleDeclarationImpl dec = entry.getRule().getStyle();
1463 style.applyStyleFromSelector(dec, entry.getSelector());
1464 }
1465 }
1466
1467 private CSSStyleSheetImpl.CSSStyleSheetRuleIndex getRuleIndex() {
1468 final CSSStyleSheetImpl styleSheet = getWrappedSheet();
1469 CSSStyleSheetImpl.CSSStyleSheetRuleIndex index = styleSheet.getRuleIndex();
1470
1471 if (index == null) {
1472 index = new CSSStyleSheetImpl.CSSStyleSheetRuleIndex();
1473 final CSSRuleListImpl ruleList = styleSheet.getCssRules();
1474 index(index, ruleList, new HashSet<>());
1475
1476 styleSheet.setRuleIndex(index);
1477 }
1478 return index;
1479 }
1480
1481 private void index(final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index, final CSSRuleListImpl ruleList,
1482 final Set<String> alreadyProcessing) {
1483
1484 for (final AbstractCSSRuleImpl rule : ruleList.getRules()) {
1485 if (rule instanceof CSSStyleRuleImpl) {
1486 final CSSStyleRuleImpl styleRule = (CSSStyleRuleImpl) rule;
1487 final SelectorList selectors = styleRule.getSelectors();
1488 for (final Selector selector : selectors) {
1489 final SimpleSelector simpleSel = selector.getSimpleSelector();
1490 if (SelectorType.ELEMENT_NODE_SELECTOR == simpleSel.getSelectorType()) {
1491 final ElementSelector es = (ElementSelector) simpleSel;
1492 boolean wasClass = false;
1493 final List<Condition> conds = es.getConditions();
1494 if (conds != null && conds.size() == 1) {
1495 final Condition c = conds.get(0);
1496 if (ConditionType.CLASS_CONDITION == c.getConditionType()) {
1497 index.addClassSelector(es, c.getValue(), selector, styleRule);
1498 wasClass = true;
1499 }
1500 }
1501 if (!wasClass) {
1502 index.addElementSelector(es, selector, styleRule);
1503 }
1504 }
1505 else {
1506 index.addOtherSelector(selector, styleRule);
1507 }
1508 }
1509 }
1510 else if (rule instanceof CSSImportRuleImpl) {
1511 final CSSImportRuleImpl importRule = (CSSImportRuleImpl) rule;
1512
1513 final CssStyleSheet sheet = getImportedStyleSheet(importRule);
1514
1515 if (!alreadyProcessing.contains(sheet.getUri())) {
1516 final CSSRuleListImpl sheetRuleList = sheet.getWrappedSheet().getCssRules();
1517 alreadyProcessing.add(sheet.getUri());
1518
1519 final MediaListImpl mediaList = importRule.getMedia();
1520 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1521 index(index, sheetRuleList, alreadyProcessing);
1522 }
1523 else {
1524 index(index.addMedia(mediaList), sheetRuleList, alreadyProcessing);
1525 }
1526 }
1527 }
1528 else if (rule instanceof CSSMediaRuleImpl) {
1529 final CSSMediaRuleImpl mediaRule = (CSSMediaRuleImpl) rule;
1530 final MediaListImpl mediaList = mediaRule.getMediaList();
1531 if (mediaList.getLength() == 0 && index.getMediaList().getLength() == 0) {
1532 index(index, mediaRule.getCssRules(), alreadyProcessing);
1533 }
1534 else {
1535 index(index.addMedia(mediaList), mediaRule.getCssRules(), alreadyProcessing);
1536 }
1537 }
1538 }
1539 }
1540
1541 private List<CSSStyleSheetImpl.SelectorEntry> selects(
1542 final CSSStyleSheetImpl.CSSStyleSheetRuleIndex index,
1543 final BrowserVersion browserVersion, final DomElement element,
1544 final String pseudoElement, final boolean fromQuerySelectorAll) {
1545
1546 final List<CSSStyleSheetImpl.SelectorEntry> matchingRules = new ArrayList<>();
1547
1548 if (isActive(index.getMediaList(), element.getPage().getEnclosingWindow())) {
1549 final String elementName = element.getLowercaseName();
1550 final String[] classes = StringUtils.splitAtJavaWhitespace(
1551 element.getAttributeDirect("class"));
1552 final Iterator<CSSStyleSheetImpl.SelectorEntry> iter =
1553 index.getSelectorEntriesIteratorFor(elementName, classes);
1554
1555 CSSStyleSheetImpl.SelectorEntry entry = iter.next();
1556 while (null != entry) {
1557 if (selects(browserVersion, entry.getSelector(),
1558 element, pseudoElement, fromQuerySelectorAll, false)) {
1559 matchingRules.add(entry);
1560 }
1561 entry = iter.next();
1562 }
1563
1564 for (final CSSStyleSheetImpl.CSSStyleSheetRuleIndex child : index.getChildren()) {
1565 matchingRules.addAll(selects(child, browserVersion,
1566 element, pseudoElement, fromQuerySelectorAll));
1567 }
1568 }
1569
1570 return matchingRules;
1571 }
1572 }