View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.css;
16  
17  import static org.htmlunit.BrowserVersionFeatures.CSS_BACKGROUND_INITIAL;
18  import static org.htmlunit.BrowserVersionFeatures.CSS_BACKGROUND_RGBA;
19  import static org.htmlunit.css.CssStyleSheet.FIXED;
20  import static org.htmlunit.css.CssStyleSheet.INITIAL;
21  import static org.htmlunit.css.CssStyleSheet.NONE;
22  import static org.htmlunit.css.CssStyleSheet.REPEAT;
23  import static org.htmlunit.css.CssStyleSheet.SCROLL;
24  
25  import java.io.Serializable;
26  import java.net.URL;
27  import java.util.LinkedHashMap;
28  import java.util.Map;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import org.htmlunit.BrowserVersion;
33  import org.htmlunit.BrowserVersionFeatures;
34  import org.htmlunit.css.StyleAttributes.Definition;
35  import org.htmlunit.cssparser.dom.AbstractCSSRuleImpl;
36  import org.htmlunit.html.DomElement;
37  import org.htmlunit.html.HtmlPage;
38  import org.htmlunit.html.impl.Color;
39  import org.htmlunit.util.StringUtils;
40  import org.htmlunit.util.UrlUtils;
41  
42  /**
43   * A css StyleDeclaration.
44   *
45   * @author Mike Bowler
46   * @author Christian Sell
47   * @author Daniel Gredler
48   * @author Chris Erskine
49   * @author Ahmed Ashour
50   * @author Rodney Gitzel
51   * @author Sudhan Moghe
52   * @author Ronald Brill
53   * @author Frank Danek
54   * @author Dennis Duysak
55   * @author cd alexndr
56   */
57  public abstract class AbstractCssStyleDeclaration implements Serializable {
58  
59      // the lookbehind and the possessive quantifiers avoid the (quadratic) backtracking
60      // for long sequences of digits; a match never starts inside of a number
61      private static final Pattern POSITION_PATTERN =
62              Pattern.compile("(?<!\\d)(\\d++\\s*+(%|px|cm|mm|in|pt|pc|em|ex))\\s*+"
63                      + "(\\d++\\s*+(%|px|cm|mm|in|pt|pc|em|ex)|top|bottom|center)");
64      private static final Pattern POSITION_PATTERN2 =
65              Pattern.compile("(left|right|center)\\s*(\\d+\\s*(%|px|cm|mm|in|pt|pc|em|ex)|top|bottom|center)");
66      private static final Pattern POSITION_PATTERN3 =
67              Pattern.compile("(top|bottom|center)\\s*(\\d+\\s*(%|px|cm|mm|in|pt|pc|em|ex)|left|right|center)");
68  
69      /**
70       * Returns the priority of the named style attribute, or an empty string if it is not found.
71       *
72       * @param name the name of the style attribute whose value is to be retrieved
73       * @return the named style attribute value, or an empty string if it is not found
74       */
75      public abstract String getStylePriority(String name);
76  
77      /**
78       * Returns the actual text of the style.
79       * @return the actual text of the style
80       */
81      public abstract String getCssText();
82  
83      /**
84       * Get the value for the style attribute.
85       * @param name the name
86       * @return the value
87       */
88      public abstract String getStyleAttribute(String name);
89  
90      /**
91       * Get the value for the style attribute.
92       * This impl ignores the default getDefaultValueIfEmpty flag, but there is an overload
93       * in {@link ComputedCssStyleDeclaration}.
94       * @param definition the definition
95       * @param getDefaultValueIfEmpty whether to get the default value if empty or not
96       * @return the value
97       */
98      public abstract String getStyleAttribute(Definition definition, boolean getDefaultValueIfEmpty);
99  
100     /**
101      * Indicates if the browser this is associated with has the feature.
102      * @param property the property name
103      * @return {@code false} if this browser doesn't have this feature
104      */
105     public abstract boolean hasFeature(BrowserVersionFeatures property);
106 
107     /**
108      * Returns the the {@link BrowserVersion}.
109      *
110      * @return the {@link BrowserVersion}
111      */
112     public abstract BrowserVersion getBrowserVersion();
113 
114     /**
115      * <p>Returns the value of one of the two named style attributes. If both attributes exist,
116      * the value of the attribute that was declared last is returned. If only one of the
117      * attributes exists, its value is returned. If neither attribute exists, an empty string
118      * is returned.</p>
119      *
120      * <p>The second named attribute may be shorthand for the actual desired property.
121      * The following formats are possible:</p>
122      * <ol>
123      *   <li><code>top right bottom left</code>: All values are explicit.</li>
124      *   <li><code>top right bottom</code>: Left is implicitly the same as right.</li>
125      *   <li><code>top right</code>: Left is implicitly the same as right, bottom is implicitly the same as top.</li>
126      *   <li><code>top</code>: Left, bottom and right are implicitly the same as top.</li>
127      * </ol>
128      *
129      * @param definition1 the name of the first style attribute
130      * @param definition2 the name of the second style attribute
131      * @return the value of one of the two named style attributes
132      */
133     public String getStyleAttribute(final Definition definition1, final Definition definition2) {
134         final StyleElement element1 = getStyleElement(definition1.getAttributeName());
135         final StyleElement element2 = getStyleElement(definition2.getAttributeName());
136 
137         if (element2 == null) {
138             if (element1 == null) {
139                 return "";
140             }
141             return element1.getValue();
142         }
143         if (element1 != null) {
144             if (StyleElement.compareToByImportanceAndSpecificity(element1, element2) > 0) {
145                 return element1.getValue();
146             }
147         }
148 
149         final String[] values = StringUtils.splitAtJavaWhitespace(element2.getValue());
150         if (definition1.name().contains("TOP")) {
151             if (values.length > 0) {
152                 return values[0];
153             }
154             return "";
155         }
156         else if (definition1.name().contains("RIGHT")) {
157             if (values.length > 1) {
158                 return values[1];
159             }
160             else if (values.length > 0) {
161                 return values[0];
162             }
163             return "";
164         }
165         else if (definition1.name().contains("BOTTOM")) {
166             if (values.length > 2) {
167                 return values[2];
168             }
169             else if (values.length > 0) {
170                 return values[0];
171             }
172             return "";
173         }
174         else if (definition1.name().contains("LEFT")) {
175             if (values.length > 3) {
176                 return values[3];
177             }
178             else if (values.length > 1) {
179                 return values[1];
180             }
181             else if (values.length > 0) {
182                 return values[0];
183             }
184             else {
185                 return "";
186             }
187         }
188         else {
189             throw new IllegalStateException("Unsupported definition: " + definition1);
190         }
191     }
192 
193     /**
194      * Sets the actual text of the style.
195      * @param value the new text
196      */
197     public abstract void setCssText(String value);
198 
199     /**
200      * Sets the specified style attribute.
201      * @param name the attribute name (camel-cased)
202      * @param newValue the attribute value
203      * @param important important value
204      */
205     public abstract void setStyleAttribute(String name, String newValue, String important);
206 
207     /**
208      * Removes the specified style attribute, returning the value of the removed attribute.
209      * @param name the attribute name (delimiter-separated, not camel-cased)
210      * @return the removed value
211      */
212     public abstract String removeStyleAttribute(String name);
213 
214     /**
215      * Returns the {@code length} property.
216      * @return the {@code length} property
217      */
218     public abstract int getLength();
219 
220     /**
221      * Returns the name of the CSS property at the specified index.
222      *
223      * @param index the index
224      * @return the name of the CSS property at the specified index
225      */
226     public abstract String item(int index);
227 
228     /**
229      * Returns the CSSRule that is the parent of this style block or <code>null</code> if this CSSStyleDeclaration is
230      * not attached to a CSSRule.
231      * @return the CSSRule that is the parent of this style block or <code>null</code> if this CSSStyleDeclaration is
232      *      not attached to a CSSRule
233      */
234     public abstract AbstractCSSRuleImpl getParentRule();
235 
236     /**
237      * Determines the StyleElement for the given name.
238      *
239      * @param name the name of the requested StyleElement
240      * @return the StyleElement or null if not found
241      */
242     public abstract StyleElement getStyleElement(String name);
243 
244     /**
245      * Determines the StyleElement for the given name.
246      * This ignores the case of the name.
247      *
248      * @param name the name of the requested StyleElement
249      * @return the StyleElement or null if not found
250      */
251     public abstract StyleElement getStyleElementCaseInSensitive(String name);
252 
253     /**
254      * Returns a sorted map containing style elements, keyed on style element name. We use a
255      * {@link LinkedHashMap} map so that results are deterministic and are thus testable.
256      *
257      * @return a sorted map containing style elements, keyed on style element name
258      */
259     public abstract Map<String, StyleElement> getStyleMap();
260 
261     /**
262      * Returns true if this is a computed style declaration.
263      *
264      * @return true if this is a computed style declaration
265      */
266     public boolean isComputed() {
267         return false;
268     }
269 
270     /**
271      * Gets the {@code backgroundAttachment} style attribute.
272      * @return the style attribute
273      */
274     public String getBackgroundAttachment() {
275         String value = getStyleAttribute(Definition.BACKGROUND_ATTACHMENT, false);
276         if (StringUtils.isBlank(value)) {
277             final String bg = getStyleAttribute(Definition.BACKGROUND, true);
278             if (StringUtils.isNotBlank(bg)) {
279                 value = findAttachment(bg);
280                 if (value == null) {
281                     if (hasFeature(CSS_BACKGROUND_INITIAL) && !isComputed()) {
282                         return INITIAL;
283                     }
284                     return SCROLL; // default if shorthand is used
285                 }
286                 return value;
287             }
288             return "";
289         }
290 
291         return value;
292     }
293 
294     /**
295      * Gets the {@code backgroundColor} style attribute.
296      * @return the style attribute
297      */
298     public String getBackgroundColor() {
299         String value = getStyleAttribute(Definition.BACKGROUND_COLOR, false);
300         if (StringUtils.isBlank(value)) {
301             final String bg = getStyleAttribute(Definition.BACKGROUND, false);
302             if (StringUtils.isBlank(bg)) {
303                 return "";
304             }
305             value = findColor(bg);
306             if (value == null) {
307                 if (hasFeature(CSS_BACKGROUND_INITIAL)) {
308                     if (!isComputed()) {
309                         return INITIAL;
310                     }
311                     return "rgba(0, 0, 0, 0)";
312                 }
313                 if (hasFeature(CSS_BACKGROUND_RGBA)) {
314                     return "rgba(0, 0, 0, 0)";
315                 }
316                 return "transparent"; // default if shorthand is used
317             }
318             return value;
319         }
320         if (StringUtils.isBlank(value)) {
321             return "";
322         }
323         return value;
324     }
325 
326     /**
327      * Gets the {@code backgroundImage} style attribute.
328      * @return the style attribute
329      */
330     public String getBackgroundImage() {
331         String value = getStyleAttribute(Definition.BACKGROUND_IMAGE, false);
332         if (StringUtils.isBlank(value)) {
333             final String bg = getStyleAttribute(Definition.BACKGROUND, false);
334             if (StringUtils.isNotBlank(bg)) {
335                 value = findImageUrl(bg);
336                 final boolean backgroundInitial = hasFeature(CSS_BACKGROUND_INITIAL);
337                 if (value == null) {
338                     return backgroundInitial && !isComputed() ? INITIAL : NONE;
339                 }
340                 if (isComputed()) {
341                     try {
342                         value = value.substring(5, value.length() - 2);
343                         final DomElement domElement = ((ComputedCssStyleDeclaration) this).getDomElement();
344                         final HtmlPage page = domElement.getHtmlPageOrNull();
345                         // like the browsers: the url is resolved and encoded (e.g. 'img .png' -> 'img%20.png')
346                         final URL url = UrlUtils.encodeUrl(page.getFullyQualifiedUrl(value), page.getCharset());
347                         return "url(\"" + url + "\")";
348                     }
349                     catch (final Exception ignored) {
350                         // ignore
351                     }
352                 }
353                 return value;
354             }
355             return "";
356         }
357 
358         return value;
359     }
360 
361     /**
362      * Gets the {@code backgroundPosition} style attribute.
363      * @return the style attribute
364      */
365     public String getBackgroundPosition() {
366         String value = getStyleAttribute(Definition.BACKGROUND_POSITION, false);
367         if (value == null) {
368             return null;
369         }
370         if (StringUtils.isBlank(value)) {
371             final String bg = getStyleAttribute(Definition.BACKGROUND, false);
372             if (bg == null) {
373                 return null;
374             }
375             if (StringUtils.isNotBlank(bg)) {
376                 value = findPosition(bg);
377                 final boolean isInitial = hasFeature(CSS_BACKGROUND_INITIAL);
378                 if (value == null) {
379                     if (isInitial) {
380                         return isComputed() ? "" : INITIAL;
381                     }
382                     return "0% 0%";
383                 }
384                 if (isComputed()) {
385                     final String[] values = StringUtils.splitAtBlank(value);
386                     switch (values[0]) {
387                         case "left":
388                             values[0] = "0%";
389                             break;
390 
391                         case "center":
392                             values[0] = "50%";
393                             break;
394 
395                         case "right":
396                             values[0] = "100%";
397                             break;
398 
399                         default:
400                     }
401                     switch (values[1]) {
402                         case "top":
403                             values[1] = "0%";
404                             break;
405 
406                         case "center":
407                             values[1] = "50%";
408                             break;
409 
410                         case "bottom":
411                             values[1] = "100%";
412                             break;
413 
414                         default:
415                     }
416                     value = values[0] + ' ' + values[1];
417                 }
418                 return value;
419             }
420             return "";
421         }
422 
423         return value;
424     }
425 
426     /**
427      * Gets the {@code backgroundRepeat} style attribute.
428      * @return the style attribute
429      */
430     public String getBackgroundRepeat() {
431         String value = getStyleAttribute(Definition.BACKGROUND_REPEAT, false);
432         if (StringUtils.isBlank(value)) {
433             final String bg = getStyleAttribute(Definition.BACKGROUND, false);
434             if (StringUtils.isNotBlank(bg)) {
435                 value = findRepeat(bg);
436                 if (value == null) {
437                     if (hasFeature(CSS_BACKGROUND_INITIAL) && !isComputed()) {
438                         return INITIAL;
439                     }
440                     return REPEAT; // default if shorthand is used
441                 }
442                 return value;
443             }
444             return "";
445         }
446 
447         return value;
448     }
449 
450     /**
451      * Gets the {@code blockSize} style attribute.
452      * @return the style attribute
453      */
454     public String getBlockSize() {
455         return getStyleAttribute(Definition.BLOCK_SIZE, true);
456     }
457 
458     /**
459      * Gets the {@code borderBottomColor} style attribute.
460      * @return the style attribute
461      */
462     public String getBorderBottomColor() {
463         String value = getStyleAttribute(Definition.BORDER_BOTTOM_COLOR, false);
464         if (value.isEmpty()) {
465             value = findColor(getStyleAttribute(Definition.BORDER_BOTTOM, false));
466             if (value == null) {
467                 value = findColor(getStyleAttribute(Definition.BORDER, false));
468             }
469             if (value == null) {
470                 value = "";
471             }
472         }
473         return value;
474     }
475 
476     /**
477      * Gets the {@code borderBottomStyle} style attribute.
478      * @return the style attribute
479      */
480     public String getBorderBottomStyle() {
481         String value = getStyleAttribute(Definition.BORDER_BOTTOM_STYLE, false);
482         if (value.isEmpty()) {
483             value = findBorderStyle(getStyleAttribute(Definition.BORDER_BOTTOM, false));
484             if (value == null) {
485                 value = findBorderStyle(getStyleAttribute(Definition.BORDER, false));
486             }
487             if (value == null) {
488                 value = "";
489             }
490         }
491         return value;
492     }
493 
494     /**
495      * Gets the {@code borderBottomWidth} style attribute.
496      * @return the style attribute
497      */
498     public String getBorderBottomWidth() {
499         return getBorderWidth(Definition.BORDER_BOTTOM_WIDTH, Definition.BORDER_BOTTOM);
500     }
501 
502     /**
503      * Gets the {@code borderLeftColor} style attribute.
504      * @return the style attribute
505      */
506     public String getBorderLeftColor() {
507         String value = getStyleAttribute(Definition.BORDER_LEFT_COLOR, false);
508         if (value.isEmpty()) {
509             value = findColor(getStyleAttribute(Definition.BORDER_LEFT, false));
510             if (value == null) {
511                 value = findColor(getStyleAttribute(Definition.BORDER, false));
512             }
513             if (value == null) {
514                 value = "";
515             }
516         }
517         return value;
518     }
519 
520     /**
521      * Gets the {@code borderLeftStyle} style attribute.
522      * @return the style attribute
523      */
524     public String getBorderLeftStyle() {
525         String value = getStyleAttribute(Definition.BORDER_LEFT_STYLE, false);
526         if (value.isEmpty()) {
527             value = findBorderStyle(getStyleAttribute(Definition.BORDER_LEFT, false));
528             if (value == null) {
529                 value = findBorderStyle(getStyleAttribute(Definition.BORDER, false));
530             }
531             if (value == null) {
532                 value = "";
533             }
534         }
535         return value;
536     }
537 
538     /**
539      * Gets the {@code borderLeftWidth} style attribute.
540      * @return the style attribute
541      */
542     public String getBorderLeftWidth() {
543         return getBorderWidth(Definition.BORDER_LEFT_WIDTH, Definition.BORDER_LEFT);
544     }
545 
546     /**
547      * Gets the border width for the specified side.
548      * @param borderSideWidth the border side width Definition
549      * @param borderSide the border side Definition
550      * @return the width, "" if not defined
551      */
552     private String getBorderWidth(final Definition borderSideWidth, final Definition borderSide) {
553         String value = getStyleAttribute(borderSideWidth, false);
554         if (value.isEmpty()) {
555             value = findBorderWidth(getStyleAttribute(borderSide, false));
556             if (value == null) {
557                 final String borderWidth = getStyleAttribute(Definition.BORDER_WIDTH, false);
558                 if (!StringUtils.isEmptyOrNull(borderWidth)) {
559                     final String[] values = StringUtils.splitAtJavaWhitespace(borderWidth);
560                     int index = values.length;
561                     if (borderSideWidth.name().contains("TOP")) {
562                         index = 0;
563                     }
564                     else if (borderSideWidth.name().contains("RIGHT")) {
565                         index = 1;
566                     }
567                     else if (borderSideWidth.name().contains("BOTTOM")) {
568                         index = 2;
569                     }
570                     else if (borderSideWidth.name().contains("LEFT")) {
571                         index = 3;
572                     }
573                     if (index < values.length) {
574                         value = values[index];
575                     }
576                 }
577             }
578             if (value == null) {
579                 value = findBorderWidth(getStyleAttribute(Definition.BORDER, false));
580             }
581             if (value == null) {
582                 value = "";
583             }
584         }
585         return value;
586     }
587 
588     /**
589      * Gets the {@code borderRightColor} style attribute.
590      * @return the style attribute
591      */
592     public String getBorderRightColor() {
593         String value = getStyleAttribute(Definition.BORDER_RIGHT_COLOR, false);
594         if (value.isEmpty()) {
595             value = findColor(getStyleAttribute(Definition.BORDER_RIGHT, false));
596             if (value == null) {
597                 value = findColor(getStyleAttribute(Definition.BORDER, false));
598             }
599             if (value == null) {
600                 value = "";
601             }
602         }
603         return value;
604     }
605 
606     /**
607      * Gets the {@code borderRightStyle} style attribute.
608      * @return the style attribute
609      */
610     public String getBorderRightStyle() {
611         String value = getStyleAttribute(Definition.BORDER_RIGHT_STYLE, false);
612         if (value.isEmpty()) {
613             value = findBorderStyle(getStyleAttribute(Definition.BORDER_RIGHT, false));
614             if (value == null) {
615                 value = findBorderStyle(getStyleAttribute(Definition.BORDER, false));
616             }
617             if (value == null) {
618                 value = "";
619             }
620         }
621         return value;
622     }
623 
624     /**
625      * Gets the {@code borderRightWidth} style attribute.
626      * @return the style attribute
627      */
628     public String getBorderRightWidth() {
629         return getBorderWidth(Definition.BORDER_RIGHT_WIDTH, Definition.BORDER_RIGHT);
630     }
631 
632     /**
633      * Gets the {@code borderTop} style attribute.
634      * @return the style attribute
635      */
636     public String getBorderTop() {
637         return getStyleAttribute(Definition.BORDER_TOP, true);
638     }
639 
640     /**
641      * Gets the {@code borderTopColor} style attribute.
642      * @return the style attribute
643      */
644     public String getBorderTopColor() {
645         String value = getStyleAttribute(Definition.BORDER_TOP_COLOR, false);
646         if (value.isEmpty()) {
647             value = findColor(getStyleAttribute(Definition.BORDER_TOP, false));
648             if (value == null) {
649                 value = findColor(getStyleAttribute(Definition.BORDER, false));
650             }
651             if (value == null) {
652                 value = "";
653             }
654         }
655         return value;
656     }
657 
658     /**
659      * Gets the {@code borderTopStyle} style attribute.
660      * @return the style attribute
661      */
662     public String getBorderTopStyle() {
663         String value = getStyleAttribute(Definition.BORDER_TOP_STYLE, false);
664         if (value.isEmpty()) {
665             value = findBorderStyle(getStyleAttribute(Definition.BORDER_TOP, false));
666             if (value == null) {
667                 value = findBorderStyle(getStyleAttribute(Definition.BORDER, false));
668             }
669             if (value == null) {
670                 value = "";
671             }
672         }
673         return value;
674     }
675 
676     /**
677      * Gets the {@code borderTopWidth} style attribute.
678      * @return the style attribute
679      */
680     public String getBorderTopWidth() {
681         return getBorderWidth(Definition.BORDER_TOP_WIDTH, Definition.BORDER_TOP);
682     }
683 
684     /**
685      * Gets the {@code bottom} style attribute.
686      * @return the style attribute
687      */
688     public String getBottom() {
689         return getStyleAttribute(Definition.BOTTOM, true);
690     }
691 
692     /**
693      * Gets the {@code color} style attribute.
694      * @return the style attribute
695      */
696     public String getColor() {
697         return getStyleAttribute(Definition.COLOR, true);
698     }
699 
700     /**
701      * Gets the {@code cssFloat} style attribute.
702      * @return the style attribute
703      */
704     public String getCssFloat() {
705         return getStyleAttribute(Definition.FLOAT, true);
706     }
707 
708     /**
709      * Gets the {@code display} style attribute.
710      * @return the style attribute
711      */
712     public String getDisplay() {
713         return getStyleAttribute(Definition.DISPLAY, true);
714     }
715 
716     /**
717      * Gets the {@code font} style attribute.
718      * @return the style attribute
719      */
720     public String getFont() {
721         return getStyleAttribute(Definition.FONT, true);
722     }
723 
724     /**
725      * Gets the {@code fontFamily} style attribute.
726      * @return the style attribute
727      */
728     public String getFontFamily() {
729         return getStyleAttribute(Definition.FONT_FAMILY, true);
730     }
731 
732     /**
733      * Gets the {@code fontSize} style attribute.
734      * @return the style attribute
735      */
736     public String getFontSize() {
737         return getStyleAttribute(Definition.FONT_SIZE, true);
738     }
739 
740     /**
741      * Gets the {@code height} style attribute.
742      * @return the style attribute
743      */
744     public String getHeight() {
745         return getStyleAttribute(Definition.HEIGHT, true);
746     }
747 
748     /**
749      * Returns the style attribute {@code left}.
750      *
751      * @return the style attribute {@code left}
752      */
753     public String getLeft() {
754         return getStyleAttribute(Definition.LEFT, true);
755     }
756 
757     /**
758      * Returns the style attribute {@code letterSpacing}.
759      *
760      * @return the style attribute {@code letterSpacing}
761      */
762     public String getLetterSpacing() {
763         return getStyleAttribute(Definition.LETTER_SPACING, true);
764     }
765 
766     /**
767      * Returns the style attribute {@code letterSpacing}.
768      *
769      * @return the style attribute {@code lineHeight}
770      */
771     public String getLineHeight() {
772         return getStyleAttribute(Definition.LINE_HEIGHT, true);
773     }
774 
775     /**
776      * Gets the {@code margin} style attribute.
777      *
778      * @return the style attribute {@code margin}
779      */
780     public String getMargin() {
781         return getStyleAttribute(Definition.MARGIN, true);
782     }
783 
784     /**
785      * Gets the {@code marginBottom} style attribute.
786      *
787      * @return the style attribute
788      */
789     public String getMarginBottom() {
790         return getStyleAttribute(Definition.MARGIN_BOTTOM, Definition.MARGIN);
791     }
792 
793     /**
794      * Gets the {@code marginLeft} style attribute.
795      *
796      * @return the style attribute
797      */
798     public String getMarginLeft() {
799         return getStyleAttribute(Definition.MARGIN_LEFT, Definition.MARGIN);
800     }
801 
802     /**
803      * Gets the {@code marginRight} style attribute.
804      *
805      * @return the style attribute
806      */
807     public String getMarginRight() {
808         return getStyleAttribute(Definition.MARGIN_RIGHT, Definition.MARGIN);
809     }
810 
811     /**
812      * Gets the {@code marginTop} style attribute.
813      *
814      * @return the style attribute
815      */
816     public String getMarginTop() {
817         return getStyleAttribute(Definition.MARGIN_TOP, Definition.MARGIN);
818     }
819 
820     /**
821      * Gets the {@code maxHeight} style attribute.
822      *
823      * @return the style attribute {@code maxHeight}
824      */
825     public String getMaxHeight() {
826         return getStyleAttribute(Definition.MAX_HEIGHT, true);
827     }
828 
829     /**
830      * Gets the {@code maxWidth} style attribute.
831      *
832      * @return the style attribute {@code maxWidth}
833      */
834     public String getMaxWidth() {
835         return getStyleAttribute(Definition.MAX_WIDTH, true);
836     }
837 
838     /**
839      * Gets the {@code minHeight} style attribute.
840      *
841      * @return the style attribute {@code minHeight}
842      */
843     public String getMinHeight() {
844         return getStyleAttribute(Definition.MIN_HEIGHT, true);
845     }
846 
847     /**
848      * Gets the {@code minWidth} style attribute.
849      *
850      * @return the style attribute {@code minWidth}
851      */
852     public String getMinWidth() {
853         return getStyleAttribute(Definition.MIN_WIDTH, true);
854     }
855 
856     /**
857      * Gets the {@code opacity} style attribute.
858      *
859      * @return the style attribute
860      */
861     public String getOpacity() {
862         final String opacity = getStyleAttribute(Definition.OPACITY, false);
863         if (opacity == null || opacity.isEmpty()) {
864             return "";
865         }
866 
867         final String trimmedOpacity = opacity.trim();
868         try {
869             final double value = Double.parseDouble(trimmedOpacity);
870             if (value % 1 == 0) {
871                 return Integer.toString((int) value);
872             }
873             return Double.toString(value);
874         }
875         catch (final NumberFormatException ignored) {
876             // ignore wrong values
877         }
878         return "";
879     }
880 
881     /**
882      * Gets the {@code orphans} style attribute.
883      *
884      * @return the style attribute {@code orphans}
885      */
886     public String getOrphans() {
887         return getStyleAttribute(Definition.ORPHANS, true);
888     }
889 
890     /**
891      * Gets the {@code outline} style attribute.
892      *
893      * @return the style attribute {@code outline}
894      */
895     public String getOutline() {
896         return getStyleAttribute(Definition.OUTLINE, true);
897     }
898 
899     /**
900      * Gets the {@code outlineWidth} style attribute.
901      *
902      * @return the style attribute {@code outlineWidth}
903      */
904     public String getOutlineWidth() {
905         return getStyleAttribute(Definition.OUTLINE_WIDTH, true);
906     }
907 
908     /**
909      * Gets the {@code padding} style attribute.
910      *
911      * @return the style attribute {@code padding}
912      */
913     public String getPadding() {
914         return getStyleAttribute(Definition.PADDING, true);
915     }
916 
917     /**
918      * Gets the {@code paddingBottom} style attribute.
919      *
920      * @return the style attribute {@code paddingBottom}
921      */
922     public String getPaddingBottom() {
923         return getStyleAttribute(Definition.PADDING_BOTTOM, Definition.PADDING);
924     }
925 
926     /**
927      * Gets the {@code paddingLeft} style attribute.
928      *
929      * @return the style attribute {@code paddingLeft}
930      */
931     public String getPaddingLeft() {
932         return getStyleAttribute(Definition.PADDING_LEFT, Definition.PADDING);
933     }
934 
935     /**
936      * Gets the {@code paddingRight} style attribute.
937      *
938      * @return the style attribute {@code paddingRight}
939      */
940     public String getPaddingRight() {
941         return getStyleAttribute(Definition.PADDING_RIGHT, Definition.PADDING);
942     }
943 
944     /**
945      * Gets the {@code paddingTop} style attribute.
946      *
947      * @return the style attribute {@code paddingTop}
948      */
949     public String getPaddingTop() {
950         return getStyleAttribute(Definition.PADDING_TOP, Definition.PADDING);
951     }
952 
953     /**
954      * Gets the {@code position} style attribute.
955      *
956      * @return the style attribute {@code position}
957      */
958     public String getPosition() {
959         return getStyleAttribute(Definition.POSITION, true);
960     }
961 
962     /**
963      * Gets the {@code right} style attribute.
964      *
965      * @return the style attribute {@code right}
966      */
967     public String getRight() {
968         return getStyleAttribute(Definition.RIGHT, true);
969     }
970 
971     /**
972      * Gets the {@code rubyAlign} style attribute.
973      *
974      * @return the style attribute {@code rubyAlign}
975      */
976     public String getRubyAlign() {
977         return getStyleAttribute(Definition.RUBY_ALIGN, true);
978     }
979 
980     /**
981      * Gets the {@code size} style attribute.
982      *
983      * @return the style attribute {@code size}
984      */
985     public String getSize() {
986         return getStyleAttribute(Definition.SIZE, true);
987     }
988 
989     /**
990      * Gets the {@code textIndent} style attribute.
991      *
992      * @return the style attribute {@code textIndent}
993      */
994     public String getTextIndent() {
995         return getStyleAttribute(Definition.TEXT_INDENT, true);
996     }
997 
998     /**
999      * Gets the {@code top} style attribute.
1000      *
1001      * @return the style attribute {@code top}
1002      */
1003     public String getTop() {
1004         return getStyleAttribute(Definition.TOP, true);
1005     }
1006 
1007     /**
1008      * Gets the {@code verticalAlign} style attribute.
1009      *
1010      * @return the style attribute {@code verticalAlign}
1011      */
1012     public String getVerticalAlign() {
1013         return getStyleAttribute(Definition.VERTICAL_ALIGN, true);
1014     }
1015 
1016     /**
1017      * Gets the {@code widows} style attribute.
1018      *
1019      * @return the style attribute {@code widows}
1020      */
1021     public String getWidows() {
1022         return getStyleAttribute(Definition.WIDOWS, true);
1023     }
1024 
1025     /**
1026      * Gets the {@code width} style attribute.
1027      *
1028      * @return the style attribute {@code width}
1029      */
1030     public String getWidth() {
1031         return getStyleAttribute(Definition.WIDTH, true);
1032     }
1033 
1034     /**
1035      * Gets the {@code wordSpacing} style attribute.
1036      *
1037      * @return the style attribute {@code wordSpacing}
1038      */
1039     public String getWordSpacing() {
1040         return getStyleAttribute(Definition.WORD_SPACING, true);
1041     }
1042 
1043     /**
1044      * Gets the {@code zIndex} style attribute.
1045      *
1046      * @return the style attribute
1047      */
1048     public String getZIndex() {
1049         final String value = getStyleAttribute(Definition.Z_INDEX_, true);
1050         try {
1051             Integer.parseInt(value);
1052             return value;
1053         }
1054         catch (final NumberFormatException e) {
1055             return "";
1056         }
1057     }
1058 
1059     /**
1060      * Searches for any attachment notation in the specified text.
1061      *
1062      * @param text the string to search in
1063      * @return the string of the attachment if found, null otherwise
1064      */
1065     private static String findAttachment(final String text) {
1066         if (text.contains(SCROLL)) {
1067             return SCROLL;
1068         }
1069         if (text.contains(FIXED)) {
1070             return FIXED;
1071         }
1072         return null;
1073     }
1074 
1075     /**
1076      * Searches for any color notation in the specified text.
1077      *
1078      * @param text the string to search in
1079      * @return the string of the color if found, null otherwise
1080      */
1081     private static String findColor(final String text) {
1082         Color tmpColor = StringUtils.findColorRGB(text);
1083         if (tmpColor != null) {
1084             return StringUtils.formatColor(tmpColor);
1085         }
1086 
1087         final String[] tokens = StringUtils.splitAtBlank(text);
1088         for (final String token : tokens) {
1089             if (CssColors.isColorKeyword(token)) {
1090                 return token;
1091             }
1092 
1093             tmpColor = StringUtils.asColorHexadecimal(token);
1094             if (tmpColor != null) {
1095                 return StringUtils.formatColor(tmpColor);
1096             }
1097         }
1098         return null;
1099     }
1100 
1101     /**
1102      * Searches for any URL notation in the specified text.
1103      *
1104      * @param text the string to search in
1105      * @return the string of the URL if found, null otherwise
1106      */
1107     private static String findImageUrl(final String text) {
1108         // This was done using the regex 'url\(\s*["']?(.*?)["']?\s*\)' but
1109         // this is vulnerable to catastrophic backtracking (e.g. 'url(' followed by many spaces).
1110         // This implementation works in linear time and follows the css rules
1111         // (https://www.w3.org/TR/css-syntax-3/#consume-url-token):
1112         // a quoted url ends at the closing quote, a url without the closing parenthesis
1113         // ends at the end of the text and a newline makes the url invalid.
1114         final int length = text.length();
1115 
1116         // the first ')' after the start of an unquoted url, the end of the url (without the trailing
1117         // white space and quote) and the last newline inside the url;
1118         // these values are shared by all 'url(' starts in front of the same ')'
1119         int closing = -1;
1120         int urlEnd = -1;
1121         int newline = -1;
1122 
1123         int start = text.indexOf("url(");
1124         while (start != -1) {
1125             int urlStart = start + 4;
1126             while (urlStart < length && isCssWhitespace(text.charAt(urlStart))) {
1127                 urlStart++;
1128             }
1129 
1130             if (urlStart < length && isQuote(text.charAt(urlStart))) {
1131                 // quoted url (a css string); ends at the closing quote or at the end of the text
1132                 final char quote = text.charAt(urlStart);
1133                 urlStart++;
1134                 int end = urlStart;
1135                 while (end < length && text.charAt(end) != quote && !isCssNewline(text.charAt(end))) {
1136                     if (text.charAt(end) == '\\') {
1137                         // escaped char
1138                         end++;
1139                     }
1140                     end++;
1141                 }
1142                 if (end >= length) {
1143                     return "url(\"" + text.substring(urlStart, length) + "\")";
1144                 }
1145                 if (text.charAt(end) == quote) {
1146                     int pos = end + 1;
1147                     while (pos < length && isCssWhitespace(text.charAt(pos))) {
1148                         pos++;
1149                     }
1150                     if (pos == length || text.charAt(pos) == ')') {
1151                         return "url(\"" + text.substring(urlStart, end) + "\")";
1152                     }
1153                 }
1154 
1155                 // invalid url (newline inside or something behind the string);
1156                 // the url can't start inside of the string
1157                 start = text.indexOf("url(", end);
1158                 continue;
1159             }
1160 
1161             if (closing < urlStart) {
1162                 closing = text.indexOf(')', urlStart);
1163                 if (closing == -1) {
1164                     // the url ends at the end of the text
1165                     closing = length;
1166                 }
1167 
1168                 urlEnd = closing;
1169                 while (urlEnd > urlStart && isCssWhitespace(text.charAt(urlEnd - 1))) {
1170                     urlEnd--;
1171                 }
1172                 if (urlEnd > urlStart && isQuote(text.charAt(urlEnd - 1))) {
1173                     urlEnd--;
1174                 }
1175 
1176                 newline = urlEnd - 1;
1177                 while (newline >= urlStart && !isCssNewline(text.charAt(newline))) {
1178                     newline--;
1179                 }
1180             }
1181 
1182             // a newline inside of the url makes the url invalid
1183             if (newline < urlStart) {
1184                 return "url(\"" + text.substring(urlStart, Math.max(urlEnd, urlStart)) + "\")";
1185             }
1186             start = text.indexOf("url(", start + 1);
1187         }
1188         return null;
1189     }
1190 
1191     /**
1192      * Checks if the given char is a css white space (space, tab or newline).
1193      *
1194      * @param c the char to check
1195      * @return true if the char is a css white space
1196      * @see <a href="https://www.w3.org/TR/css-syntax-3/#whitespace">CSS Syntax - whitespace</a>
1197      */
1198     private static boolean isCssWhitespace(final char c) {
1199         return c == ' ' || c == '\t' || isCssNewline(c);
1200     }
1201 
1202     /**
1203      * Checks if the given char is a single or double quote.
1204      *
1205      * @param c the char to check
1206      * @return true if the char is a quote
1207      */
1208     private static boolean isQuote(final char c) {
1209         return c == '"' || c == '\'';
1210     }
1211 
1212     /**
1213      * Checks if the given char is a css newline (line feed, carriage return or form feed).
1214      *
1215      * @param c the char to check
1216      * @return true if the char is a css newline
1217      * @see <a href="https://www.w3.org/TR/css-syntax-3/#newline">CSS Syntax - newline</a>
1218      */
1219     private static boolean isCssNewline(final char c) {
1220         return c == '\n' || c == '\r' || c == '\f';
1221     }
1222 
1223     /**
1224      * Searches for any position notation in the specified text.
1225      *
1226      * @param text the string to search in
1227      * @return the string of the position if found, null otherwise
1228      */
1229     private static String findPosition(final String text) {
1230         Matcher m = POSITION_PATTERN.matcher(text);
1231         if (m.find()) {
1232             return m.group(1) + " " + m.group(3);
1233         }
1234         m = POSITION_PATTERN2.matcher(text);
1235         if (m.find()) {
1236             return m.group(1) + " " + m.group(2);
1237         }
1238         m = POSITION_PATTERN3.matcher(text);
1239         if (m.find()) {
1240             return m.group(2) + " " + m.group(1);
1241         }
1242         return null;
1243     }
1244 
1245     /**
1246      * Searches for any repeat notation in the specified text.
1247      *
1248      * @param text the string to search in
1249      * @return the string of the repeat if found, null otherwise
1250      */
1251     private static String findRepeat(final String text) {
1252         if (text.contains("repeat-x")) {
1253             return "repeat-x";
1254         }
1255         if (text.contains("repeat-y")) {
1256             return "repeat-y";
1257         }
1258         if (text.contains("no-repeat")) {
1259             return "no-repeat";
1260         }
1261         if (text.contains(REPEAT)) {
1262             return REPEAT;
1263         }
1264         return null;
1265     }
1266 
1267     /**
1268      * Searches for a border style in the specified text.
1269      *
1270      * @param text the string to search in
1271      * @return the border style if found, null otherwise
1272      */
1273     private static String findBorderStyle(final String text) {
1274         for (final String token : StringUtils.splitAtBlank(text)) {
1275             if (isBorderStyle(token)) {
1276                 return token;
1277             }
1278         }
1279         return null;
1280     }
1281 
1282     /**
1283      * Returns if the specified token is a border style.
1284      *
1285      * @param token the token to check
1286      * @return whether the token is a border style or not
1287      */
1288     private static boolean isBorderStyle(final String token) {
1289         return NONE.equalsIgnoreCase(token) || "hidden".equalsIgnoreCase(token)
1290             || "dotted".equalsIgnoreCase(token) || "dashed".equalsIgnoreCase(token)
1291             || "solid".equalsIgnoreCase(token) || "double".equalsIgnoreCase(token)
1292             || "groove".equalsIgnoreCase(token) || "ridge".equalsIgnoreCase(token)
1293             || "inset".equalsIgnoreCase(token) || "outset".equalsIgnoreCase(token);
1294     }
1295 
1296     /**
1297      * Searches for a border width in the specified text.
1298      *
1299      * @param text the string to search in
1300      * @return the border width if found, null otherwise
1301      */
1302     private static String findBorderWidth(final String text) {
1303         for (final String token : StringUtils.splitAtBlank(text)) {
1304             if (isBorderWidth(token)) {
1305                 return token;
1306             }
1307         }
1308         return null;
1309     }
1310 
1311     /**
1312      * Returns if the specified token is a border width.
1313      *
1314      * @param token the token to check
1315      * @return whether the token is a border width or not
1316      */
1317     private static boolean isBorderWidth(final String token) {
1318         return "thin".equalsIgnoreCase(token) || "medium".equalsIgnoreCase(token)
1319             || "thick".equalsIgnoreCase(token) || isLength(token);
1320     }
1321 
1322     /**
1323      * Returns if the specified token is a length.
1324      *
1325      * @param token the token to check
1326      * @return whether the token is a length or not
1327      */
1328     static boolean isLength(final String token) {
1329         if (token.endsWith("%")) {
1330             try {
1331                 Double.parseDouble(token.substring(0, token.length() - 1));
1332                 return true;
1333             }
1334             catch (final NumberFormatException ignored) {
1335                 // ignore wrong values
1336             }
1337             return false;
1338         }
1339 
1340         if (token.endsWith("em") || token.endsWith("ex") || token.endsWith("px") || token.endsWith("in")
1341             || token.endsWith("cm") || token.endsWith("mm") || token.endsWith("pt") || token.endsWith("pc")) {
1342 
1343             try {
1344                 Double.parseDouble(token.substring(0, token.length() - 2));
1345                 return true;
1346             }
1347             catch (final NumberFormatException ignored) {
1348                 // ignore wrong values
1349             }
1350             return false;
1351         }
1352 
1353         return false;
1354     }
1355 }