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