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.JS_CLIENTHEIGHT_INPUT_17;
18  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_INPUT_18;
19  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_RADIO_CHECKBOX_14;
20  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_RB_17;
21  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_RT_9;
22  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_RUBY_17;
23  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTWIDTH_INPUT_TEXT_157;
24  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTWIDTH_INPUT_TEXT_173;
25  import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTWIDTH_RADIO_CHECKBOX_14;
26  import static org.htmlunit.css.CssStyleSheet.ABSOLUTE;
27  import static org.htmlunit.css.CssStyleSheet.AUTO;
28  import static org.htmlunit.css.CssStyleSheet.BLOCK;
29  import static org.htmlunit.css.CssStyleSheet.FIXED;
30  import static org.htmlunit.css.CssStyleSheet.INHERIT;
31  import static org.htmlunit.css.CssStyleSheet.INLINE;
32  import static org.htmlunit.css.CssStyleSheet.NONE;
33  import static org.htmlunit.css.CssStyleSheet.RELATIVE;
34  import static org.htmlunit.css.CssStyleSheet.SCROLL;
35  import static org.htmlunit.css.CssStyleSheet.STATIC;
36  
37  import java.util.EnumSet;
38  import java.util.HashSet;
39  import java.util.Map;
40  import java.util.Set;
41  import java.util.SortedMap;
42  import java.util.TreeMap;
43  
44  import org.htmlunit.BrowserVersion;
45  import org.htmlunit.BrowserVersionFeatures;
46  import org.htmlunit.Page;
47  import org.htmlunit.SgmlPage;
48  import org.htmlunit.WebWindow;
49  import org.htmlunit.css.StyleAttributes.Definition;
50  import org.htmlunit.cssparser.dom.AbstractCSSRuleImpl;
51  import org.htmlunit.cssparser.dom.CSSStyleDeclarationImpl;
52  import org.htmlunit.cssparser.dom.Property;
53  import org.htmlunit.cssparser.parser.selector.Selector;
54  import org.htmlunit.cssparser.parser.selector.SelectorSpecificity;
55  import org.htmlunit.html.BaseFrameElement;
56  import org.htmlunit.html.DomElement;
57  import org.htmlunit.html.DomNode;
58  import org.htmlunit.html.DomText;
59  import org.htmlunit.html.HtmlAbbreviated;
60  import org.htmlunit.html.HtmlAcronym;
61  import org.htmlunit.html.HtmlAddress;
62  import org.htmlunit.html.HtmlArticle;
63  import org.htmlunit.html.HtmlAside;
64  import org.htmlunit.html.HtmlBaseFont;
65  import org.htmlunit.html.HtmlBidirectionalIsolation;
66  import org.htmlunit.html.HtmlBidirectionalOverride;
67  import org.htmlunit.html.HtmlBig;
68  import org.htmlunit.html.HtmlBody;
69  import org.htmlunit.html.HtmlBold;
70  import org.htmlunit.html.HtmlButton;
71  import org.htmlunit.html.HtmlButtonInput;
72  import org.htmlunit.html.HtmlCanvas;
73  import org.htmlunit.html.HtmlCenter;
74  import org.htmlunit.html.HtmlCheckBoxInput;
75  import org.htmlunit.html.HtmlCitation;
76  import org.htmlunit.html.HtmlCode;
77  import org.htmlunit.html.HtmlData;
78  import org.htmlunit.html.HtmlDefinition;
79  import org.htmlunit.html.HtmlDefinitionDescription;
80  import org.htmlunit.html.HtmlDefinitionTerm;
81  import org.htmlunit.html.HtmlDivision;
82  import org.htmlunit.html.HtmlElement;
83  import org.htmlunit.html.HtmlElement.DisplayStyle;
84  import org.htmlunit.html.HtmlEmphasis;
85  import org.htmlunit.html.HtmlFigure;
86  import org.htmlunit.html.HtmlFigureCaption;
87  import org.htmlunit.html.HtmlFileInput;
88  import org.htmlunit.html.HtmlFooter;
89  import org.htmlunit.html.HtmlFrame;
90  import org.htmlunit.html.HtmlHeader;
91  import org.htmlunit.html.HtmlHeading1;
92  import org.htmlunit.html.HtmlHeading2;
93  import org.htmlunit.html.HtmlHeading3;
94  import org.htmlunit.html.HtmlHeading4;
95  import org.htmlunit.html.HtmlHeading5;
96  import org.htmlunit.html.HtmlHeading6;
97  import org.htmlunit.html.HtmlHiddenInput;
98  import org.htmlunit.html.HtmlImage;
99  import org.htmlunit.html.HtmlInlineFrame;
100 import org.htmlunit.html.HtmlInput;
101 import org.htmlunit.html.HtmlItalic;
102 import org.htmlunit.html.HtmlKeyboard;
103 import org.htmlunit.html.HtmlLayer;
104 import org.htmlunit.html.HtmlLegend;
105 import org.htmlunit.html.HtmlMain;
106 import org.htmlunit.html.HtmlMark;
107 import org.htmlunit.html.HtmlNav;
108 import org.htmlunit.html.HtmlNoBreak;
109 import org.htmlunit.html.HtmlNoEmbed;
110 import org.htmlunit.html.HtmlNoFrames;
111 import org.htmlunit.html.HtmlNoLayer;
112 import org.htmlunit.html.HtmlNoScript;
113 import org.htmlunit.html.HtmlOutput;
114 import org.htmlunit.html.HtmlPage;
115 import org.htmlunit.html.HtmlPasswordInput;
116 import org.htmlunit.html.HtmlPlainText;
117 import org.htmlunit.html.HtmlRadioButtonInput;
118 import org.htmlunit.html.HtmlRb;
119 import org.htmlunit.html.HtmlResetInput;
120 import org.htmlunit.html.HtmlRp;
121 import org.htmlunit.html.HtmlRt;
122 import org.htmlunit.html.HtmlRtc;
123 import org.htmlunit.html.HtmlRuby;
124 import org.htmlunit.html.HtmlS;
125 import org.htmlunit.html.HtmlSample;
126 import org.htmlunit.html.HtmlSection;
127 import org.htmlunit.html.HtmlSelect;
128 import org.htmlunit.html.HtmlSlot;
129 import org.htmlunit.html.HtmlSmall;
130 import org.htmlunit.html.HtmlSpan;
131 import org.htmlunit.html.HtmlStrike;
132 import org.htmlunit.html.HtmlStrong;
133 import org.htmlunit.html.HtmlSubmitInput;
134 import org.htmlunit.html.HtmlSubscript;
135 import org.htmlunit.html.HtmlSummary;
136 import org.htmlunit.html.HtmlSuperscript;
137 import org.htmlunit.html.HtmlTableCell;
138 import org.htmlunit.html.HtmlTableRow;
139 import org.htmlunit.html.HtmlTeletype;
140 import org.htmlunit.html.HtmlTextArea;
141 import org.htmlunit.html.HtmlTextInput;
142 import org.htmlunit.html.HtmlTime;
143 import org.htmlunit.html.HtmlUnderlined;
144 import org.htmlunit.html.HtmlUnknownElement;
145 import org.htmlunit.html.HtmlVariable;
146 import org.htmlunit.html.HtmlWordBreak;
147 import org.htmlunit.platform.Platform;
148 import org.htmlunit.util.StringUtils;
149 
150 /**
151  * An object for a CSSStyleDeclaration, which is computed.
152  *
153  * @see org.htmlunit.javascript.host.Window#getComputedStyle(Object, String)
154  *
155  * @author Ahmed Ashour
156  * @author Marc Guillemot
157  * @author Ronald Brill
158  * @author Frank Danek
159  * @author Alex Gorbatovsky
160  * @author cd alexndr
161  */
162 @SuppressWarnings("PMD.AvoidDuplicateLiterals")
163 public class ComputedCssStyleDeclaration extends AbstractCssStyleDeclaration {
164 
165     /** The set of 'inheritable' definitions. */
166     private static final Set<Definition> INHERITABLE_DEFINITIONS = EnumSet.of(
167         Definition.BORDER_COLLAPSE,
168         Definition.BORDER_SPACING,
169         Definition.CAPTION_SIDE,
170         Definition.COLOR,
171         Definition.CURSOR,
172         Definition.DIRECTION,
173         Definition.EMPTY_CELLS,
174         Definition.FONT_FAMILY,
175         Definition.FONT_SIZE,
176         Definition.FONT_STYLE,
177         Definition.FONT_VARIANT,
178         Definition.FONT_WEIGHT,
179         Definition.FONT,
180         Definition.LETTER_SPACING,
181         Definition.LINE_HEIGHT,
182         Definition.LIST_STYLE_IMAGE,
183         Definition.LIST_STYLE_POSITION,
184         Definition.LIST_STYLE_TYPE,
185         Definition.LIST_STYLE,
186         Definition.ORPHANS,
187         Definition.QUOTES,
188         Definition.SPEAK,
189         Definition.TEXT_ALIGN,
190         Definition.TEXT_INDENT,
191         Definition.TEXT_TRANSFORM,
192         Definition.VISIBILITY,
193         Definition.WHITE_SPACE,
194         Definition.WIDOWS,
195         Definition.WORD_SPACING);
196 
197     /** Default font sizes for heading elements, used when no explicit font-size is set. */
198     private static final Map<Class<? extends HtmlElement>, String> HEADING_DEFAULT_FONT_SIZES = Map.of(
199         HtmlHeading1.class, "32px",
200         HtmlHeading2.class, "24px",
201         HtmlHeading3.class, "19px",
202         HtmlHeading4.class, "16px",
203         HtmlHeading5.class, "13px",
204         HtmlHeading6.class, "11px");
205 
206     private static final Set<Class<? extends HtmlElement>> ZERO_HEIGHT_ELEMENT_CLASSES = Set.of(
207             HtmlAbbreviated.class,
208             HtmlAcronym.class,
209             HtmlAddress.class,
210             HtmlArticle.class,
211             HtmlAside.class,
212             HtmlBaseFont.class,
213             HtmlBidirectionalIsolation.class,
214             HtmlBidirectionalOverride.class,
215             HtmlBig.class,
216             HtmlBold.class,
217             HtmlCenter.class,
218             HtmlCitation.class,
219             HtmlCode.class,
220             HtmlData.class,
221             HtmlDefinition.class,
222             HtmlDefinitionDescription.class,
223             HtmlDefinitionTerm.class,
224             HtmlDivision.class,
225             HtmlEmphasis.class,
226             HtmlFigure.class,
227             HtmlFigureCaption.class,
228             HtmlFooter.class,
229             HtmlHeader.class,
230             HtmlItalic.class,
231             HtmlKeyboard.class,
232             HtmlLayer.class,
233             HtmlLegend.class,
234             HtmlMain.class,
235             HtmlMark.class,
236             HtmlNav.class,
237             HtmlNoBreak.class,
238             HtmlNoEmbed.class,
239             HtmlNoFrames.class,
240             HtmlNoLayer.class,
241             HtmlNoScript.class,
242             HtmlOutput.class,
243             HtmlPlainText.class,
244             HtmlRp.class,
245             HtmlRtc.class,
246             HtmlS.class,
247             HtmlSample.class,
248             HtmlSection.class,
249             HtmlSlot.class,
250             HtmlSmall.class,
251             HtmlStrike.class,
252             HtmlStrong.class,
253             HtmlSubscript.class,
254             HtmlSummary.class,
255             HtmlSuperscript.class,
256             HtmlTeletype.class,
257             HtmlTime.class,
258             HtmlUnderlined.class,
259             HtmlUnknownElement.class,
260             HtmlVariable.class,
261             HtmlWordBreak.class
262         );
263 
264     /** Denotes a value which should be returned as is. */
265     public static final String EMPTY_FINAL = new String("");
266 
267     /** The computed, cached width of the element to which this computed style belongs (no padding, borders, etc.). */
268     private int cachedWidth_ = Integer.MIN_VALUE;
269 
270     /** The computed, cached content width (sum/max of children's widths) of the element. */
271     private int cachedContentWidth_ = Integer.MIN_VALUE;
272 
273     /**
274      * The computed, cached height of the element to which this computed style belongs (no padding, borders, etc.),
275      * taking child elements into account.
276      */
277     private int cachedHeight_ = Integer.MIN_VALUE;
278 
279     /**
280      * The computed, cached height of the element to which this computed style belongs (no padding, borders, etc.),
281      * <b>not</b> taking child elements into account.
282      */
283     private int cachedEmptyHeight_ = Integer.MIN_VALUE;
284 
285     /** The computed, cached top of the element to which this computed style belongs. */
286     private int cachedTop_ = Integer.MIN_VALUE;
287 
288     /** The computed, cached horizontal padding (left + right) of the element to which this computed style belongs. */
289     private int cachedPaddingHorizontal_ = Integer.MIN_VALUE;
290 
291     /** The computed, cached vertical padding (top + bottom) of the element to which this computed style belongs. */
292     private int cachedPaddingVertical_ = Integer.MIN_VALUE;
293 
294     /** The computed, cached horizontal border (left + right) of the element to which this computed style belongs. */
295     private int cachedBorderHorizontal_ = Integer.MIN_VALUE;
296 
297     /** The computed, cached vertical border (top + bottom) of the element to which this computed style belongs. */
298     private int cachedBorderVertical_ = Integer.MIN_VALUE;
299 
300     /**
301      * Local modifications maintained here rather than in the element. We use a sorted
302      * map so that results are deterministic and thus easily testable.
303      */
304     private final SortedMap<String, StyleElement> localModifications_ = new TreeMap<>();
305 
306     /** The wrapped CSSStyleDeclaration. */
307     private final ElementCssStyleDeclaration elementStyleDeclaration_;
308 
309     /**
310      * Ctor.
311      * @param styleDeclaration the {@link ElementCssStyleDeclaration} this is based on
312      */
313     public ComputedCssStyleDeclaration(final ElementCssStyleDeclaration styleDeclaration) {
314         super();
315         elementStyleDeclaration_ = styleDeclaration;
316         elementStyleDeclaration_.getDomElement().setDefaults(this);
317     }
318 
319     /**
320      * {@inheritDoc}
321      */
322     @Override
323     public String getStylePriority(final String name) {
324         return elementStyleDeclaration_.getStylePriority(name);
325     }
326 
327     /**
328      * {@inheritDoc}
329      */
330     @Override
331     public String getCssText() {
332         return elementStyleDeclaration_.getCssText();
333     }
334 
335     /**
336      * {@inheritDoc}
337      */
338     @Override
339     public String getStyleAttribute(final String name) {
340         final StyleElement element = getStyleElement(name);
341         if (element != null && element.getValue() != null) {
342             final String value = element.getValue();
343             if (!"content".equals(name)
344                     && !value.contains("url")) {
345                 return StringUtils.toRootLowerCase(value);
346             }
347             return value;
348         }
349         return "";
350     }
351 
352     /**
353      * {@inheritDoc}
354      */
355     @Override
356     public String getStyleAttribute(final Definition definition, final boolean getDefaultValueIfEmpty) {
357         final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
358         final boolean isDefInheritable = INHERITABLE_DEFINITIONS.contains(definition);
359 
360         // to make the fuzzer happy the recursion was removed
361         final ComputedCssStyleDeclaration[] queue = {this};
362         String value = null;
363         while (queue[0] != null) {
364             value = getStyleAttributeWorker(definition, getDefaultValueIfEmpty,
365                                             browserVersion, isDefInheritable, queue);
366         }
367 
368         return value;
369     }
370 
371     /**
372      * Internal worker to resolve inherited or default style attributes recursively without stack recursion.
373      *
374      * @param definition the CSS style attribute definition
375      * @param getDefaultValueIfEmpty whether to fall back to the default computed value if unassigned
376      * @param browserVersion the current target browser version
377      * @param isDefInheritable true if the style definition is an inheritable property
378      * @param queue single-element array used as a pointer queue for loop-based parent traversal
379      * @return the resolved computed style attribute string
380      */
381     private static String getStyleAttributeWorker(final Definition definition,
382                 final boolean getDefaultValueIfEmpty, final BrowserVersion browserVersion,
383                 final boolean isDefInheritable, final ComputedCssStyleDeclaration[] queue) {
384         final ComputedCssStyleDeclaration decl = queue[0];
385         queue[0] = null;
386 
387         final DomElement domElem = decl.getDomElement();
388         if (!domElem.isAttachedToPage()) {
389             return EMPTY_FINAL;
390         }
391 
392         String value = decl.getStyleAttribute(definition.getAttributeName());
393         if (value.isEmpty()) {
394             final DomNode parent = domElem.getParentNode();
395             if (isDefInheritable && parent instanceof DomElement element) {
396                 final WebWindow window = domElem.getPage().getEnclosingWindow();
397 
398                 queue[0] = window.getComputedStyle(element, null);
399             }
400             else if (getDefaultValueIfEmpty) {
401                 value = definition.getDefaultComputedValue(browserVersion);
402             }
403         }
404 
405         return value;
406     }
407 
408     /**
409      * Return the string, or {@code toReturnIfEmptyOrDefault}.
410      *
411      * @param toReturnIfEmptyOrDefault the value to return if empty or equals the {@code defaultValue}
412      * @param defaultValue the default value of the string
413      * @return the string, or {@code toReturnIfEmptyOrDefault}
414      */
415     private String getStyleAttribute(final Definition definition, final String toReturnIfEmptyOrDefault,
416             final String defaultValue) {
417         final DomElement domElement = getDomElement();
418 
419         if (!domElement.isAttachedToPage()) {
420             return EMPTY_FINAL;
421         }
422 
423         final boolean isDefInheritable = INHERITABLE_DEFINITIONS.contains(definition);
424 
425         // to make the fuzzer happy the recursion was removed
426         final BrowserVersion browserVersion = domElement.getPage().getWebClient().getBrowserVersion();
427         final ComputedCssStyleDeclaration[] queue = {this};
428         String value = null;
429         while (queue[0] != null) {
430             value = getStyleAttributeWorker(definition, false, browserVersion, isDefInheritable, queue);
431         }
432 
433         if (value == null || value.isEmpty() || value.equals(defaultValue)) {
434             return toReturnIfEmptyOrDefault;
435         }
436 
437         return value;
438     }
439 
440     /**
441      * {@inheritDoc}
442      */
443     @Override
444     public void setCssText(final String value) {
445         // read only
446     }
447 
448     /**
449      * {@inheritDoc}
450      */
451     @Override
452     public void setStyleAttribute(final String name, final String newValue, final String important) {
453         // read only
454     }
455 
456     /**
457      * {@inheritDoc}
458      */
459     @Override
460     public String removeStyleAttribute(final String name) {
461         // read only
462         return null;
463     }
464 
465     /**
466      * {@inheritDoc}
467      */
468     @Override
469     public int getLength() {
470         return elementStyleDeclaration_.getLength();
471     }
472 
473     /**
474      * Returns the width.
475      *
476      * @return the width
477      */
478     @Override
479     public String getWidth() {
480         if (NONE.equals(getDisplay())) {
481             return AUTO;
482         }
483 
484         final DomElement domElem = getDomElement();
485         if (!domElem.isAttachedToPage()) {
486             return "";
487         }
488 
489         final int windowWidth = domElem.getPage().getEnclosingWindow().getInnerWidth();
490         return CssPixelValueConverter.pixelString(domElem,
491                 style -> {
492                     final String value = style.getStyleAttribute(Definition.WIDTH, true);
493                     if (StringUtils.isEmptyOrNull(value)) {
494                         final String position = getStyleAttribute(Definition.POSITION, true);
495                         if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
496                             final String content = domElem.getVisibleText();
497                             // do this only for small content
498                             // at least for empty div's this is more correct
499                             if (null != content && content.length() < 13) {
500                                 return (content.length() * 7) + "px";
501                             }
502                         }
503 
504                         int windowDefaultValue = windowWidth;
505                         if (domElem instanceof HtmlBody) {
506                             windowDefaultValue -= 16;
507                         }
508                         return windowDefaultValue + "px";
509                     }
510                     else if (AUTO.equals(value)) {
511                         int windowDefaultValue = windowWidth;
512                         if (domElem instanceof HtmlBody) {
513                             windowDefaultValue -= 16;
514                         }
515                         return windowDefaultValue + "px";
516                     }
517 
518                     return value;
519                 },
520                 () -> 0,
521                 () -> windowWidth);
522     }
523 
524     /**
525      * {@inheritDoc}
526      */
527     @Override
528     public String item(final int index) {
529         return elementStyleDeclaration_.item(index);
530     }
531 
532     /**
533      * {@inheritDoc}
534      */
535     @Override
536     public AbstractCSSRuleImpl getParentRule() {
537         return elementStyleDeclaration_.getParentRule();
538     }
539 
540     /**
541      * {@inheritDoc}
542      */
543     @Override
544     public StyleElement getStyleElement(final String name) {
545         final StyleElement existent = elementStyleDeclaration_.getStyleElement(name);
546 
547         final StyleElement localStyleMod = localModifications_.get(name);
548         if (localStyleMod == null) {
549             return existent;
550         }
551 
552         if (existent == null) {
553             // Local modifications represent either default style elements or style elements
554             // defined in stylesheets; either way, they shouldn't overwrite any style
555             // elements derived directly from the HTML element's "style" attribute.
556             return localStyleMod;
557         }
558 
559         // replace if !IMPORTANT
560         if (StyleElement.PRIORITY_IMPORTANT.equals(localStyleMod.getPriority())) {
561             if (existent.isImportant()) {
562                 if (existent.getSpecificity().compareTo(localStyleMod.getSpecificity()) < 0) {
563                     return localStyleMod;
564                 }
565             }
566             else {
567                 return localStyleMod;
568             }
569         }
570         return existent;
571     }
572 
573     /**
574      * {@inheritDoc}
575      */
576     @Override
577     public StyleElement getStyleElementCaseInSensitive(final String name) {
578         return elementStyleDeclaration_.getStyleElementCaseInSensitive(name);
579     }
580 
581     /**
582      * {@inheritDoc}
583      */
584     @Override
585     public Map<String, StyleElement> getStyleMap() {
586         return elementStyleDeclaration_.getStyleMap();
587     }
588 
589     /**
590      * Returns the {@link DomElement} the backing {@link ElementCssStyleDeclaration}
591      * is associated with.
592      *
593      * @return the {@link DomElement} the backing {@link ElementCssStyleDeclaration}
594      *         is associated with
595      */
596     public DomElement getDomElement() {
597         return elementStyleDeclaration_.getDomElement();
598     }
599 
600     /**
601      * {@inheritDoc}
602      */
603     @Override
604     public String getBackgroundAttachment() {
605         return defaultIfEmpty(super.getBackgroundAttachment(), Definition.BACKGROUND_ATTACHMENT);
606     }
607 
608     /**
609      * {@inheritDoc}
610      */
611     @Override
612     public String getBackgroundColor() {
613         if (!getDomElement().isAttachedToPage()) {
614             return EMPTY_FINAL;
615         }
616 
617         final String value = super.getBackgroundColor();
618         if (StringUtils.isEmptyOrNull(value)) {
619             return Definition.BACKGROUND_COLOR.getDefaultComputedValue(getBrowserVersion());
620         }
621         return CssColors.toRGBColor(value);
622     }
623 
624     /**
625      * {@inheritDoc}
626      */
627     @Override
628     public String getBackgroundImage() {
629         return defaultIfEmpty(super.getBackgroundImage(), Definition.BACKGROUND_IMAGE);
630     }
631 
632     /**
633      * Gets the {@code backgroundPosition} style attribute.
634      * @return the style attribute
635      */
636     @Override
637     public String getBackgroundPosition() {
638         return defaultIfEmpty(super.getBackgroundPosition(), Definition.BACKGROUND_POSITION);
639     }
640 
641     /**
642      * {@inheritDoc}
643      */
644     @Override
645     public String getBackgroundRepeat() {
646         return defaultIfEmpty(super.getBackgroundRepeat(), Definition.BACKGROUND_REPEAT);
647     }
648 
649     /**
650      * {@inheritDoc}
651      */
652     @Override
653     public String getBlockSize() {
654         if (NONE.equals(getDisplay())) {
655             return defaultIfEmpty(super.getBlockSize(), Definition.BLOCK_SIZE);
656         }
657 
658         final DomElement domElem = getDomElement();
659         if (!domElem.isAttachedToPage()) {
660             return defaultIfEmpty(super.getBlockSize(), Definition.BLOCK_SIZE);
661         }
662 
663         return CssPixelValueConverter.pixelString(domElem,
664                 style -> {
665                     final String value = style.getStyleAttribute(Definition.HEIGHT, true);
666                     if (StringUtils.isEmptyOrNull(value)) {
667                         final String content = domElem.getVisibleText();
668                         // do this only for small content
669                         // at least for empty div's this is more correct
670                         if (content == null) {
671                             return "0px";
672                         }
673                         return calculateIntrinsicHeight(domElem) + "px";
674                     }
675                     return value;
676                 },
677                 () -> 0,
678                 () -> 0);
679     }
680 
681     /**
682      * {@inheritDoc}
683      */
684     @Override
685     public String getBorderBottomColor() {
686         return defaultIfEmpty(super.getBorderBottomColor(), Definition.BORDER_BOTTOM_COLOR);
687     }
688 
689     /**
690      * {@inheritDoc}
691      */
692     @Override
693     public String getBorderBottomStyle() {
694         return defaultIfEmpty(super.getBorderBottomStyle(), Definition.BORDER_BOTTOM_STYLE);
695     }
696 
697     /**
698      * {@inheritDoc}
699      */
700     @Override
701     public String getBorderBottomWidth() {
702         return pixelString(defaultIfEmpty(super.getBorderBottomWidth(), Definition.BORDER_BOTTOM_WIDTH));
703     }
704 
705     /**
706      * {@inheritDoc}
707      */
708     @Override
709     public String getBorderLeftColor() {
710         return defaultIfEmpty(super.getBorderLeftColor(), Definition.BORDER_LEFT_COLOR);
711     }
712 
713     /**
714      * {@inheritDoc}
715      */
716     @Override
717     public String getBorderLeftStyle() {
718         return defaultIfEmpty(super.getBorderLeftStyle(), Definition.BORDER_LEFT_STYLE);
719     }
720 
721     /**
722      * {@inheritDoc}
723      */
724     @Override
725     public String getBorderLeftWidth() {
726         return pixelString(defaultIfEmpty(super.getBorderLeftWidth(), "0px", null));
727     }
728 
729     /**
730      * {@inheritDoc}
731      */
732     @Override
733     public String getBorderRightColor() {
734         return defaultIfEmpty(super.getBorderRightColor(), "rgb(0, 0, 0)", null);
735     }
736 
737     /**
738      * {@inheritDoc}
739      */
740     @Override
741     public String getBorderRightStyle() {
742         return defaultIfEmpty(super.getBorderRightStyle(), NONE, null);
743     }
744 
745     /**
746      * {@inheritDoc}
747      */
748     @Override
749     public String getBorderRightWidth() {
750         return pixelString(defaultIfEmpty(super.getBorderRightWidth(), "0px", null));
751     }
752 
753     /**
754      * {@inheritDoc}
755      */
756     @Override
757     public String getBorderTopColor() {
758         return defaultIfEmpty(super.getBorderTopColor(), "rgb(0, 0, 0)", null);
759     }
760 
761     /**
762      * {@inheritDoc}
763      */
764     @Override
765     public String getBorderTopStyle() {
766         return defaultIfEmpty(super.getBorderTopStyle(), NONE, null);
767     }
768 
769     /**
770      * {@inheritDoc}
771      */
772     @Override
773     public String getBorderTopWidth() {
774         return pixelString(defaultIfEmpty(super.getBorderTopWidth(), "0px", null));
775     }
776 
777     /**
778      * {@inheritDoc}
779      */
780     @Override
781     public String getBottom() {
782         return getStyleAttribute(Definition.BOTTOM, AUTO, null);
783     }
784 
785     /**
786      * {@inheritDoc}
787      */
788     @Override
789     public String getColor() {
790         final String value = getStyleAttribute(Definition.COLOR, "rgb(0, 0, 0)", null);
791         return CssColors.toRGBColor(value);
792     }
793 
794     /**
795      * {@inheritDoc}
796      */
797     @Override
798     public String getCssFloat() {
799         return defaultIfEmpty(super.getCssFloat(), Definition.CSS_FLOAT);
800     }
801 
802     /**
803      * {@inheritDoc}
804      */
805     @Override
806     public String getDisplay() {
807         final DomElement domElem = getDomElement();
808         if (!domElem.isAttachedToPage()) {
809             return "";
810         }
811 
812         final HtmlElement htmlElem = (domElem instanceof HtmlElement e) ? e : null;
813 
814         if (htmlElem != null && htmlElem.isHidden()) {
815             return DisplayStyle.NONE.value();
816         }
817 
818         // don't use defaultIfEmpty for performance
819         // (no need to calculate the default if not empty)
820         final String value = getStyleAttribute(Definition.DISPLAY.getAttributeName());
821         if (StringUtils.isEmptyOrNull(value)) {
822             return htmlElem != null ? htmlElem.getDefaultStyleDisplay().value() : "";
823         }
824 
825         return value;
826     }
827 
828     /**
829      * {@inheritDoc}
830      */
831     @Override
832     public String getFont() {
833         final DomElement domElem = getDomElement();
834         if (domElem.isAttachedToPage()) {
835             return getStyleAttribute(Definition.FONT, true);
836         }
837         return "";
838     }
839 
840     /**
841      * {@inheritDoc}
842      */
843     @Override
844     public String getFontFamily() {
845         return getStyleAttribute(Definition.FONT_FAMILY, true);
846     }
847 
848     /**
849      * {@inheritDoc}
850      */
851     @Override
852     public String getFontSize() {
853         return getStyleAttribute(Definition.FONT_SIZE, true);
854     }
855 
856     /**
857      * {@inheritDoc}
858      */
859     @Override
860     public String getLineHeight() {
861         return defaultIfEmpty(super.getLineHeight(), Definition.LINE_HEIGHT);
862     }
863 
864     /**
865      * {@inheritDoc}
866      */
867     @Override
868     public String getHeight() {
869         if (NONE.equals(getDisplay())) {
870             return AUTO;
871         }
872 
873         final DomElement elem = getDomElement();
874         if (!elem.isAttachedToPage()) {
875             return "";
876         }
877 
878         final ComputedCssStyleDeclaration style = elem.getPage().getEnclosingWindow().getComputedStyle(elem, null);
879         final String styleValue = style.getStyleAttribute(Definition.HEIGHT, true);
880 
881         if (styleValue == null || styleValue.isEmpty() || AUTO.equals(styleValue) || styleValue.endsWith("%")) {
882             return style.getCalculatedHeight(false, false) + "px";
883         }
884 
885         if (styleValue.endsWith("px")) {
886             return styleValue;
887         }
888 
889         return CssPixelValueConverter.pixelValue(styleValue) + "px";
890     }
891 
892     /**
893      * {@inheritDoc}
894      */
895     @Override
896     public String getLeft() {
897         if (NONE.equals(getDisplay())) {
898             return AUTO;
899         }
900 
901         final DomElement elem = getDomElement();
902         if (!elem.isAttachedToPage()) {
903             return "";
904         }
905 
906         final String superLeft = super.getLeft();
907         if (!superLeft.endsWith("%")) {
908             return defaultIfEmpty(superLeft, AUTO, null);
909         }
910 
911         return CssPixelValueConverter.pixelString(elem,
912                 style -> {
913                     if (style.getDomElement() == elem) {
914                         return style.getStyleAttribute(Definition.LEFT, true);
915                     }
916                     return style.getStyleAttribute(Definition.WIDTH, true);
917                 },
918                 () -> 0,
919                 () -> 0);
920     }
921 
922     /**
923      * {@inheritDoc}
924      */
925     @Override
926     public String getLetterSpacing() {
927         return defaultIfEmpty(super.getLetterSpacing(), "normal", null);
928     }
929 
930     /**
931      * {@inheritDoc}
932      */
933     @Override
934     public String getMargin() {
935         return defaultIfEmpty(super.getMargin(), Definition.MARGIN, true);
936     }
937 
938     /**
939      * {@inheritDoc}
940      */
941     @Override
942     public String getMarginBottom() {
943         return pixelString(defaultIfEmpty(super.getMarginBottom(), "0px", null));
944     }
945 
946     /**
947      * {@inheritDoc}
948      */
949     @Override
950     public String getMarginLeft() {
951         return getMarginX(super.getMarginLeft(), Definition.MARGIN_LEFT);
952     }
953 
954     /**
955      * {@inheritDoc}
956      */
957     @Override
958     public String getMarginRight() {
959         return getMarginX(super.getMarginRight(), Definition.MARGIN_RIGHT);
960     }
961 
962     private String getMarginX(final String superMarginX, final Definition definition) {
963         if (!superMarginX.endsWith("%")) {
964             return pixelString(defaultIfEmpty(superMarginX, "0px", null));
965         }
966         final DomElement element = getDomElement();
967         if (!element.isAttachedToPage()) {
968             return "";
969         }
970 
971         final int windowWidth = element.getPage().getEnclosingWindow().getInnerWidth();
972         return CssPixelValueConverter.pixelString(element,
973                 style -> {
974                     if (style.getDomElement() == element) {
975                         return style.getStyleAttribute(definition, true);
976                     }
977                     return style.getStyleAttribute(Definition.WIDTH, true);
978                 },
979                 () -> 0,
980                 () -> windowWidth);
981     }
982 
983     /**
984      * {@inheritDoc}
985      */
986     @Override
987     public String getMarginTop() {
988         return pixelString(defaultIfEmpty(super.getMarginTop(), "0px", null));
989     }
990 
991     /**
992      * {@inheritDoc}
993      */
994     @Override
995     public String getMaxHeight() {
996         return defaultIfEmpty(super.getMaxHeight(), NONE, null);
997     }
998 
999     /**
1000      * {@inheritDoc}
1001      */
1002     @Override
1003     public String getMaxWidth() {
1004         return defaultIfEmpty(super.getMaxWidth(), NONE, null);
1005     }
1006 
1007     /**
1008      * {@inheritDoc}
1009      */
1010     @Override
1011     public String getMinHeight() {
1012         return defaultIfEmpty(super.getMinHeight(), "0px", null);
1013     }
1014 
1015     /**
1016      * {@inheritDoc}
1017      */
1018     @Override
1019     public String getMinWidth() {
1020         return defaultIfEmpty(super.getMinWidth(), "0px", null);
1021     }
1022 
1023     /**
1024      * {@inheritDoc}
1025      */
1026     @Override
1027     public String getOpacity() {
1028         return defaultIfEmpty(super.getOpacity(), "1", null);
1029     }
1030 
1031     /**
1032      * {@inheritDoc}
1033      */
1034     @Override
1035     public String getOrphans() {
1036         return defaultIfEmpty(super.getOrphans(), Definition.ORPHANS);
1037     }
1038 
1039     /**
1040      * {@inheritDoc}
1041      */
1042     @Override
1043     public String getOutlineWidth() {
1044         return defaultIfEmpty(super.getOutlineWidth(), "0px", null);
1045     }
1046 
1047     /**
1048      * {@inheritDoc}
1049      */
1050     @Override
1051     public String getPadding() {
1052         return defaultIfEmpty(super.getPadding(), Definition.PADDING, true);
1053     }
1054 
1055     /**
1056      * {@inheritDoc}
1057      */
1058     @Override
1059     public String getPaddingBottom() {
1060         return pixelString(defaultIfEmpty(super.getPaddingBottom(), "0px", null));
1061     }
1062 
1063     /**
1064      * {@inheritDoc}
1065      */
1066     @Override
1067     public String getPaddingLeft() {
1068         return pixelString(defaultIfEmpty(super.getPaddingLeft(), "0px", null));
1069     }
1070 
1071     /**
1072      * {@inheritDoc}
1073      */
1074     @Override
1075     public String getPaddingRight() {
1076         return pixelString(defaultIfEmpty(super.getPaddingRight(), "0px", null));
1077     }
1078 
1079     /**
1080      * {@inheritDoc}
1081      */
1082     @Override
1083     public String getPaddingTop() {
1084         return pixelString(defaultIfEmpty(super.getPaddingTop(), "0px", null));
1085     }
1086 
1087     /**
1088      * {@inheritDoc}
1089      */
1090     @Override
1091     public String getRight() {
1092         return defaultIfEmpty(super.getRight(), AUTO, null);
1093     }
1094 
1095     /**
1096      * {@inheritDoc}
1097      */
1098     @Override
1099     public String getTextIndent() {
1100         return defaultIfEmpty(super.getTextIndent(), "0px", null);
1101     }
1102 
1103     /**
1104      * {@inheritDoc}
1105      */
1106     @Override
1107     public String getTop() {
1108         if (NONE.equals(getDisplay())) {
1109             return AUTO;
1110         }
1111 
1112         final DomElement elem = getDomElement();
1113         if (!elem.isAttachedToPage()) {
1114             return "";
1115         }
1116 
1117         final String superTop = super.getTop();
1118         if (!superTop.endsWith("%")) {
1119             return defaultIfEmpty(superTop, Definition.TOP);
1120         }
1121 
1122         return CssPixelValueConverter.pixelString(elem,
1123                 style -> {
1124                     if (style.getDomElement() == elem) {
1125                         return style.getStyleAttribute(Definition.TOP, true);
1126                     }
1127                     return style.getStyleAttribute(Definition.HEIGHT, true);
1128                 },
1129                 () -> 0,
1130                 () -> 0);
1131     }
1132 
1133     /**
1134      * Returns the computed top (Y coordinate), relative to the node's parent's top edge.
1135      * @param includeMargin whether or not to take the margin into account in the calculation
1136      * @param includeBorder whether or not to take the border into account in the calculation
1137      * @param includePadding whether or not to take the padding into account in the calculation
1138      * @return the computed top (Y coordinate), relative to the node's parent's top edge
1139      */
1140     public int getTop(final boolean includeMargin, final boolean includeBorder, final boolean includePadding) {
1141         final Integer cachedTop = getCachedTop();
1142 
1143         int top = 0;
1144         if (cachedTop == Integer.MIN_VALUE) {
1145             final String position = getPositionWithInheritance();
1146             if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1147                 top = getTopForAbsolutePositionWithInheritance();
1148             }
1149             else if (getDomElement() instanceof HtmlTableCell) {
1150                 top = 0;
1151             }
1152             else {
1153                 // Calculate the vertical displacement caused by *previous* siblings.
1154                 DomNode prev = getDomElement().getPreviousSibling();
1155                 boolean prevHadComputedTop = false;
1156                 while (prev != null && !prevHadComputedTop) {
1157                     if (prev instanceof HtmlElement) {
1158                         final ComputedCssStyleDeclaration style =
1159                                 prev.getPage().getEnclosingWindow().getComputedStyle((DomElement) prev, null);
1160 
1161                         // only previous block elements are counting
1162                         final String display = style.getDisplay();
1163                         if (isBlock(display)) {
1164                             int prevTop = 0;
1165                             final Integer eCachedTop = style.getCachedTop();
1166                             if (eCachedTop == Integer.MIN_VALUE) {
1167                                 final String prevPosition = style.getPositionWithInheritance();
1168                                 if (ABSOLUTE.equals(prevPosition) || FIXED.equals(prevPosition)) {
1169                                     prevTop += style.getTopForAbsolutePositionWithInheritance();
1170                                 }
1171                                 else {
1172                                     if (RELATIVE.equals(prevPosition)) {
1173                                         final String t = style.getTopWithInheritance();
1174                                         prevTop += CssPixelValueConverter.pixelValue(t);
1175                                     }
1176                                 }
1177                             }
1178                             else {
1179                                 prevHadComputedTop = true;
1180                                 prevTop += eCachedTop.intValue();
1181                             }
1182                             prevTop += style.getCalculatedHeight(true, true);
1183                             final int margin = CssPixelValueConverter.pixelValue(style.getMarginTop());
1184                             prevTop += margin;
1185                             top += prevTop;
1186                         }
1187                     }
1188                     prev = prev.getPreviousSibling();
1189                 }
1190                 // If the position is relative, we also need to add the specified "top" displacement.
1191                 if (RELATIVE.equals(position)) {
1192                     final String t = getTopWithInheritance();
1193                     top += CssPixelValueConverter.pixelValue(t);
1194                 }
1195             }
1196             setCachedTop(top);
1197         }
1198         else {
1199             top = cachedTop;
1200         }
1201 
1202         if (includeMargin) {
1203             final int margin = CssPixelValueConverter.pixelValue(getMarginTop());
1204             top += margin;
1205         }
1206 
1207         if (includeBorder) {
1208             final int border = CssPixelValueConverter.pixelValue(getBorderTopWidth());
1209             top += border;
1210         }
1211 
1212         if (includePadding) {
1213             final int padding = getPaddingTopValue();
1214             top += padding;
1215         }
1216 
1217         return top;
1218     }
1219 
1220     private static boolean isBlock(final String display) {
1221         return display != null
1222                 && !INLINE.equals(display)
1223                 && !NONE.equals(display);
1224     }
1225 
1226     /**
1227      * Returns the CSS {@code top} attribute, replacing inherited values with the actual parent values.
1228      * @return the CSS {@code top} attribute, replacing inherited values with the actual parent values
1229      */
1230     public String getTopWithInheritance() {
1231         String top = getTop();
1232         if (INHERIT.equals(top)) {
1233             final HtmlElement parent = (HtmlElement) getDomElement().getParentNode();
1234             if (parent == null) {
1235                 top = AUTO;
1236             }
1237             else {
1238                 final ComputedCssStyleDeclaration style =
1239                         parent.getPage().getEnclosingWindow().getComputedStyle(parent, null);
1240                 top = style.getTopWithInheritance();
1241             }
1242         }
1243         return top;
1244     }
1245 
1246     /**
1247      * Returns the CSS {@code bottom} attribute, replacing inherited values with the actual parent values.
1248      * @return the CSS {@code bottom} attribute, replacing inherited values with the actual parent values
1249      */
1250     public String getBottomWithInheritance() {
1251         String bottom = getBottom();
1252         if (INHERIT.equals(bottom)) {
1253             final DomNode parent = getDomElement().getParentNode();
1254             if (parent == null) {
1255                 bottom = AUTO;
1256             }
1257             else {
1258                 final ComputedCssStyleDeclaration style =
1259                         parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
1260                 bottom = style.getBottomWithInheritance();
1261             }
1262         }
1263         return bottom;
1264     }
1265 
1266     /**
1267      * {@inheritDoc}
1268      */
1269     @Override
1270     public String getVerticalAlign() {
1271         return defaultIfEmpty(super.getVerticalAlign(), "baseline", null);
1272     }
1273 
1274     /**
1275      * {@inheritDoc}
1276      */
1277     @Override
1278     public String getWidows() {
1279         return defaultIfEmpty(super.getWidows(), Definition.WIDOWS);
1280     }
1281 
1282     /**
1283      * {@inheritDoc}
1284      */
1285     @Override
1286     public String getWordSpacing() {
1287         return defaultIfEmpty(super.getWordSpacing(), Definition.WORD_SPACING);
1288     }
1289 
1290     /**
1291      * {@inheritDoc}
1292      */
1293     @Override
1294     public String getZIndex() {
1295         if (!getDomElement().isAttachedToPage()) {
1296             return EMPTY_FINAL;
1297         }
1298 
1299         final String response = super.getZIndex();
1300         if (response.isEmpty()) {
1301             return AUTO;
1302         }
1303         return response;
1304     }
1305 
1306     /**
1307      * Gets the left margin of the element.
1308      * @return the value in pixels
1309      */
1310     public int getMarginLeftValue() {
1311         return CssPixelValueConverter.pixelValue(getMarginLeft());
1312     }
1313 
1314     /**
1315      * Gets the right margin of the element.
1316      * @return the value in pixels
1317      */
1318     public int getMarginRightValue() {
1319         return CssPixelValueConverter.pixelValue(getMarginRight());
1320     }
1321 
1322     /**
1323      * Gets the top margin of the element.
1324      * @return the value in pixels
1325      */
1326     public int getMarginTopValue() {
1327         return CssPixelValueConverter.pixelValue(getMarginTop());
1328     }
1329 
1330     /**
1331      * Gets the bottom margin of the element.
1332      * @return the value in pixels
1333      */
1334     public int getMarginBottomValue() {
1335         return CssPixelValueConverter.pixelValue(getMarginBottom());
1336     }
1337 
1338     /**
1339      * Returns the computed left (X coordinate), relative to the node's parent's left edge.
1340      * @param includeMargin whether or not to take the margin into account in the calculation
1341      * @param includeBorder whether or not to take the border into account in the calculation
1342      * @param includePadding whether or not to take the padding into account in the calculation
1343      * @return the computed left (X coordinate), relative to the node's parent's left edge
1344      */
1345     public int getLeft(final boolean includeMargin, final boolean includeBorder, final boolean includePadding) {
1346         final String p = getPositionWithInheritance();
1347         final String l = getLeftWithInheritance();
1348         final String r = getRightWithInheritance();
1349 
1350         int left;
1351         if ((ABSOLUTE.equals(p) || FIXED.equals(p)) && !AUTO.equals(l)) {
1352             // No need to calculate displacement caused by sibling nodes.
1353             left = CssPixelValueConverter.pixelValue(l);
1354         }
1355         else if ((ABSOLUTE.equals(p) || FIXED.equals(p)) && !AUTO.equals(r)) {
1356             // Need to calculate the horizontal displacement caused by *all* siblings.
1357             final DomNode parent = getDomElement().getParentNode();
1358             final int parentWidth;
1359             if (parent == null) {
1360                 parentWidth = getDomElement().getPage().getEnclosingWindow().getInnerWidth();
1361             }
1362             else if (parent instanceof Page page) {
1363                 parentWidth = page.getEnclosingWindow().getInnerWidth();
1364             }
1365             else {
1366                 final ComputedCssStyleDeclaration parentStyle =
1367                         parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
1368                 parentWidth = parentStyle.getCalculatedWidth(false, false);
1369             }
1370             left = parentWidth - CssPixelValueConverter.pixelValue(r);
1371         }
1372         else if (FIXED.equals(p) && !AUTO.equals(r)) {
1373             final DomElement e = getDomElement();
1374             final WebWindow win = e.getPage().getEnclosingWindow();
1375             final ComputedCssStyleDeclaration style = win.getComputedStyle(e, null);
1376 
1377             final DomNode parent = e.getParentNode();
1378             final int parentWidth;
1379             if (parent == null) {
1380                 parentWidth = win.getInnerWidth();
1381             }
1382             else {
1383                 final ComputedCssStyleDeclaration parentStyle = win.getComputedStyle((DomElement) parent, null);
1384                 parentWidth = CssPixelValueConverter.pixelValue(parentStyle.getWidth())
1385                                 - CssPixelValueConverter.pixelValue(style.getWidth());
1386             }
1387             left = parentWidth - CssPixelValueConverter.pixelValue(r);
1388         }
1389         else if (FIXED.equals(p) && AUTO.equals(l)) {
1390             // Fixed to the location at which the browser puts it via normal element flowing.
1391             final DomNode parent = getDomElement().getParentNode();
1392             if (parent == null || parent instanceof Page) {
1393                 left = 0;
1394             }
1395             else {
1396                 final ComputedCssStyleDeclaration style =
1397                         parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
1398                 left = CssPixelValueConverter.pixelValue(style.getLeftWithInheritance());
1399             }
1400         }
1401         else if (STATIC.equals(p)) {
1402             // We need to calculate the horizontal displacement caused by *previous* siblings.
1403             left = 0;
1404             DomNode prev = getDomElement().getPreviousSibling();
1405             while (prev != null) {
1406                 if (prev instanceof HtmlElement) {
1407                     final ComputedCssStyleDeclaration style =
1408                             prev.getPage().getEnclosingWindow().getComputedStyle((DomElement) prev, null);
1409                     final String d = style.getDisplay();
1410                     if (isBlock(d)) {
1411                         break;
1412                     }
1413                     else if (!NONE.equals(d)) {
1414                         left += style.getCalculatedWidth(true, true);
1415                     }
1416                 }
1417                 else if (prev instanceof DomText) {
1418                     final String content = prev.getVisibleText();
1419                     if (content != null) {
1420                         left += content.trim().length()
1421                                 * getDomElement().getPage().getWebClient().getBrowserVersion().getPixelsPerChar();
1422                     }
1423                 }
1424                 prev = prev.getPreviousSibling();
1425             }
1426         }
1427         else {
1428             // Just use the CSS specified value.
1429             left = CssPixelValueConverter.pixelValue(l);
1430         }
1431 
1432         if (includeMargin) {
1433             final int margin = getMarginLeftValue();
1434             left += margin;
1435         }
1436 
1437         if (includeBorder) {
1438             final int border = CssPixelValueConverter.pixelValue(getBorderLeftWidth());
1439             left += border;
1440         }
1441 
1442         if (includePadding) {
1443             final int padding = getPaddingLeftValue();
1444             left += padding;
1445         }
1446 
1447         return left;
1448     }
1449 
1450     /**
1451      * {@inheritDoc}
1452      */
1453     @Override
1454     public String getPosition() {
1455         return defaultIfEmpty(super.getPosition(), Definition.POSITION);
1456     }
1457 
1458     /**
1459      * Returns the CSS {@code position} attribute, replacing inherited values with the actual parent values.
1460      * @return the CSS {@code position} attribute, replacing inherited values with the actual parent values
1461      */
1462     public String getPositionWithInheritance() {
1463         String p = getStyleAttribute(Definition.POSITION, true);
1464         if (INHERIT.equals(p)) {
1465             final DomNode parent = getDomElement().getParentNode();
1466             if (parent == null) {
1467                 p = STATIC;
1468             }
1469             else {
1470                 final ComputedCssStyleDeclaration style =
1471                         parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
1472                 p = style.getPositionWithInheritance();
1473             }
1474         }
1475         return p;
1476     }
1477 
1478     /**
1479      * Returns the CSS {@code left} attribute, replacing inherited values with the actual parent values.
1480      * @return the CSS {@code left} attribute, replacing inherited values with the actual parent values
1481      */
1482     public String getLeftWithInheritance() {
1483         String left = getLeft();
1484         if (INHERIT.equals(left)) {
1485             final DomNode parent = getDomElement().getParentNode();
1486             if (parent == null) {
1487                 left = AUTO;
1488             }
1489             else {
1490                 final ComputedCssStyleDeclaration style =
1491                         parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
1492                 left = style.getLeftWithInheritance();
1493             }
1494         }
1495         return left;
1496     }
1497 
1498     /**
1499      * Returns the CSS {@code right} attribute, replacing inherited values with the actual parent values.
1500      * @return the CSS {@code right} attribute, replacing inherited values with the actual parent values
1501      */
1502     public String getRightWithInheritance() {
1503         String right = getRight();
1504         if (INHERIT.equals(right)) {
1505             final DomNode parent = getDomElement().getParentNode();
1506             if (parent == null) {
1507                 right = AUTO;
1508             }
1509             else {
1510                 final ComputedCssStyleDeclaration style =
1511                         parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
1512                 right = style.getRightWithInheritance();
1513             }
1514         }
1515         return right;
1516     }
1517 
1518     private int getTopForAbsolutePositionWithInheritance() {
1519         final String t = getTopWithInheritance();
1520 
1521         if (!AUTO.equals(t)) {
1522             // No need to calculate displacement caused by sibling nodes.
1523             return CssPixelValueConverter.pixelValue(t);
1524         }
1525 
1526         final String b = getBottomWithInheritance();
1527         if (!AUTO.equals(b)) {
1528             // Estimate the vertical displacement caused by *all* siblings.
1529             // This is very rough, and doesn't even take position or display types into account.
1530             // It also doesn't take into account the fact that the parent's height may be hardcoded in CSS.
1531             int top = 0;
1532             DomNode child = getDomElement().getParentNode().getFirstChild();
1533             while (child != null) {
1534                 if (child instanceof HtmlElement && child.mayBeDisplayed()) {
1535                     top += 20;
1536                 }
1537                 child = child.getNextSibling();
1538             }
1539             top -= CssPixelValueConverter.pixelValue(b);
1540             return top;
1541         }
1542 
1543         return 0;
1544     }
1545 
1546     /**
1547      * Returns the element's height, possibly including its padding and border.
1548      * @param includeBorder whether or not to include the border height in the returned value
1549      * @param includePadding whether or not to include the padding height in the returned value
1550      * @return the element's height, possibly including its padding and border
1551      */
1552     public int getCalculatedHeight(final boolean includeBorder, final boolean includePadding) {
1553         final DomElement element = getDomElement();
1554 
1555         if (!element.isAttachedToPage()) {
1556             return 0;
1557         }
1558         int height = getCalculatedHeight(element);
1559         if (!"border-box".equals(getStyleAttribute(Definition.BOX_SIZING, true))) {
1560             if (includeBorder) {
1561                 height += getBorderVertical();
1562             }
1563             else if (isScrollable(element, true, true) && !(element instanceof HtmlBody)) {
1564                 height -= 17;
1565             }
1566 
1567             if (includePadding) {
1568                 height += getPaddingVertical();
1569             }
1570         }
1571         return height;
1572     }
1573 
1574     /**
1575      * Returns the element's calculated height, taking both relevant CSS and the element's children into account.
1576      * @return the element's calculated height, taking both relevant CSS and the element's children into account
1577      */
1578     private int getCalculatedHeight(final DomElement element) {
1579         final int cachedHeight = getCachedHeight();
1580         if (cachedHeight != Integer.MIN_VALUE) {
1581             return cachedHeight;
1582         }
1583 
1584         if (element instanceof HtmlImage image) {
1585             return updateCachedHeight(image.getHeightOrDefault());
1586         }
1587 
1588         final boolean isInline = INLINE.equals(getDisplay()) && !(element instanceof HtmlInlineFrame);
1589         final String styleHeight = super.getHeight();
1590 
1591         // an EXPLICIT, non-auto height wins outright -- mirrors what
1592         // getBlockSize() already does for the same underlying style value,
1593         // and what getCalculatedWidth() does for an explicit width
1594         if (!isInline
1595                 && !StringUtils.isEmptyOrNull(styleHeight)
1596                 && !AUTO.equals(styleHeight)) {
1597             return updateCachedHeight(
1598                     CssPixelValueConverter.pixelValue(element,
1599                             style -> style.getStyleAttribute(Definition.HEIGHT, true),
1600                             () -> 0,
1601                             () -> element.getPage().getEnclosingWindow().getInnerHeight()));
1602         }
1603 
1604         // height is ignored for inline elements, and AUTO/unset falls back to
1605         // content-based sizing
1606         if (isInline || StringUtils.isEmptyOrNull(styleHeight) || AUTO.equals(styleHeight)) {
1607             final int contentHeight = getContentHeight();
1608             if (contentHeight > 0) {
1609                 return updateCachedHeight(contentHeight);
1610             }
1611         }
1612 
1613         return updateCachedHeight(calculateIntrinsicHeight(element));
1614     }
1615 
1616     /**
1617      * Returns the element's width in pixels, possibly including its padding and border.
1618      *
1619      * @param includeBorder whether or not to include the border width in the returned value
1620      * @param includePadding whether or not to include the padding width in the returned value
1621      * @return the element's width in pixels, possibly including its padding and border
1622      */
1623     public int getCalculatedWidth(final boolean includeBorder, final boolean includePadding) {
1624         final DomElement element = getDomElement();
1625 
1626         if (!element.isAttachedToPage()) {
1627             return 0;
1628         }
1629 
1630         int width = getCalculatedWidth(element);
1631         if (!"border-box".equals(getStyleAttribute(Definition.BOX_SIZING, true))) {
1632             if (includeBorder) {
1633                 width += getBorderHorizontal();
1634             }
1635             else if (isScrollable(element, false, true) && !(element instanceof HtmlBody)) {
1636                 width -= 17;
1637             }
1638 
1639             if (includePadding) {
1640                 width += getPaddingHorizontal();
1641             }
1642         }
1643         return width;
1644     }
1645 
1646     /**
1647      * Computes and caches the "used" (rendered) width of this element in pixels,
1648      * following (a simplified approximation of) the CSS width-resolution
1649      * algorithm: explicit style widths win outright; otherwise the width falls
1650      * back through several special cases (fixed-size tags, floated/positioned
1651      * elements, block-level fill-parent, form-control defaults) before finally
1652      * shrink-wrapping to content as a last resort.
1653      *
1654      * @param element the element to compute the width for
1655      * @return the calculated width, in pixels
1656      */
1657     private int getCalculatedWidth(final DomElement element) {
1658         final int cached = getCachedWidth();
1659         if (cached != Integer.MIN_VALUE) {
1660             return cached;
1661         }
1662 
1663         // an element that isn't rendered at all has no box, and therefore no width
1664         if (!element.mayBeDisplayed()) {
1665             return updateCachedWidth(0);
1666         }
1667 
1668         final String display = getDisplay();
1669         if (NONE.equals(getDisplay())) {
1670             return updateCachedWidth(0);
1671         }
1672 
1673         final String styleWidth = getStyleAttribute(Definition.WIDTH, true);
1674         final DomNode parent = element.getParentNode();
1675 
1676         // "width" has no effect on inline elements, and the two shrink-to-fit
1677         // keywords are handled as part of the "not explicitly set" path below
1678         // rather than as an explicit pixel width
1679         final boolean widthNotExplicitlySet = INLINE.equals(display)
1680                 || StringUtils.isEmptyOrNull(styleWidth)
1681                 || AUTO.equals(styleWidth)
1682                 || isShrinkToFitKeyword(styleWidth);
1683         if (widthNotExplicitlySet && parent instanceof HtmlElement) {
1684             return updateCachedWidth(computeWidthWhenNotExplicitlySet(element, display, styleWidth, parent));
1685         }
1686 
1687         if (AUTO.equals(styleWidth)) {
1688             // no parent to size against (or width wasn't handled above) --
1689             // fall back to the window's own width
1690             return updateCachedWidth(element.getPage().getEnclosingWindow().getInnerWidth());
1691         }
1692 
1693         // width was explicitly set in the style attribute to a real value (e.g.
1694         // "200px", "50%") -- resolve it via the normal CSS pixel-value converter,
1695         // which knows how to handle relative units against the window as a
1696         // percentage base
1697         return updateCachedWidth(CssPixelValueConverter.pixelValue(element,
1698                 style -> style.getStyleAttribute(Definition.WIDTH, true),
1699                 () -> 0,
1700                 () -> element.getPage().getEnclosingWindow().getInnerWidth()));
1701     }
1702 
1703     /**
1704      * Returns whether {@code styleWidth} is one of the CSS sizing keywords that
1705      * mean "shrink-wrap to content" rather than a concrete length -- these are
1706      * treated the same as "no explicit width" for the purposes of this class'
1707      * (simplified) width algorithm.
1708      *
1709      * @param styleWidth the raw {@code width} style value to check
1710      * @return {@code true} if {@code styleWidth} is {@code max-content} or
1711      *     {@code min-content}
1712      */
1713     private static boolean isShrinkToFitKeyword(final String styleWidth) {
1714         return "max-content".equals(styleWidth) || "min-content".equals(styleWidth);
1715     }
1716 
1717     /**
1718      * Computes the width for an element whose style doesn't pin down an explicit
1719      * pixel width -- either nothing was specified, the element is inline (where
1720      * width is ignored), or a shrink-to-fit keyword was used. Tries, in order:
1721      * a fixed default for certain special tags (canvas, iframe, frame); shrink-
1722      * wrapping for floated/absolutely-or-fixed-positioned elements; filling the
1723      * containing block for an ordinary block element; a hardcoded default for
1724      * specific form-control tags; and finally shrink-wrapping to content as the
1725      * general fallback.
1726      *
1727      * @param element the element being sized
1728      * @param display the element's resolved {@code display} value
1729      * @param styleWidth the element's raw {@code width} style value
1730      * @param parent the element's parent node (known to be an {@link HtmlElement}
1731      *     by the caller)
1732      * @return the computed width, in pixels
1733      */
1734     private int computeWidthWhenNotExplicitlySet(final DomElement element, final String display,
1735             final String styleWidth, final DomNode parent) {
1736 
1737         // hack: TODO find a way to specify default values for different tags
1738         final Integer fixedDefault = getFixedDefaultWidth(element);
1739         if (fixedDefault != null) {
1740             return fixedDefault;
1741         }
1742 
1743         // an absolutely/fixed-positioned or floated element shrink-wraps to its
1744         // children regardless of display type (block, inline, etc.) -- it's
1745         // taken out of normal flow, so it never "fills" a containing block
1746         if (isFloatedOrOutOfNormalFlow()) {
1747             return shrinkToFitWidth(element);
1748         }
1749 
1750         // an ordinary block-level element (not using a shrink-to-fit keyword)
1751         // fills the full width of its containing block, per normal CSS flow
1752         if (BLOCK.equals(display) && !isShrinkToFitKeyword(styleWidth)) {
1753             return fillsParentWidth(element, parent);
1754         }
1755 
1756         final Integer formControlDefault = getDefaultWidthForFormControl(element);
1757         if (formControlDefault != null) {
1758             return formControlDefault;
1759         }
1760 
1761         // last resort: inline elements, and block elements using a shrink-to-fit
1762         // width keyword, take up however much space is required by their content
1763         return shrinkToFitWidth(element);
1764     }
1765 
1766     /**
1767      * Returns whether this element is floated or taken out of normal document
1768      * flow (absolute/fixed positioning) -- such elements always shrink-wrap to
1769      * their content, never fill their containing block, regardless of their
1770      * {@code display} value.
1771      *
1772      * @return {@code true} if this element is floated left/right, or absolutely
1773      *     or fixed positioned
1774      */
1775     private boolean isFloatedOrOutOfNormalFlow() {
1776         final String position = getStyleAttribute(Definition.POSITION, true);
1777         if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1778             return true;
1779         }
1780         final String cssFloat = getCssFloat();
1781         return "right".equals(cssFloat) || "left".equals(cssFloat);
1782     }
1783 
1784     /**
1785      * Shrink-wraps to the rendered width of the element's children, falling back
1786      * to a text-length approximation when there is no rendered child content --
1787      * for example, an element containing only a text node, which contributes no
1788      * child element box for {@link #getContentWidth()} to measure.
1789      * <p>
1790      * This fallback previously existed only in the floated/out-of-flow code
1791      * path; a block element sized via a shrink-to-fit width keyword (e.g.
1792      * {@code max-content}) reached an equivalent "shrink to content"
1793      * computation through a separate call site that lacked the fallback,
1794      * incorrectly resolving to {@code 0} for text-only content. Both call sites
1795      * now share this one implementation.
1796      * </p>
1797      *
1798      * @param element the element to shrink-wrap
1799      * @return the shrink-to-fit width, in pixels
1800      */
1801     private int shrinkToFitWidth(final DomElement element) {
1802         final int contentWidth = getContentWidth();
1803         if (contentWidth > 0) {
1804             return contentWidth;
1805         }
1806 
1807         // no rendered child content -- approximate from the element's own
1808         // visible text instead
1809         final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
1810         return element.getVisibleText().length() * browserVersion.getPixelsPerChar();
1811     }
1812 
1813     /**
1814      * Computes the width of an ordinary block-level element that fills its
1815      * containing block: the window's inner width minus a fixed margin allowance
1816      * for {@code <body>}, or the parent's own calculated width (minus this
1817      * element's own border/padding) for anything else.
1818      *
1819      * @param element the block-level element being sized
1820      * @param parent the element's parent node
1821      * @return the fill-parent width, in pixels
1822      */
1823     private int fillsParentWidth(final DomElement element, final DomNode parent) {
1824         final int windowWidth = element.getPage().getEnclosingWindow().getInnerWidth();
1825         if (element instanceof HtmlBody) {
1826             // approximate default body margin
1827             return windowWidth - 16;
1828         }
1829 
1830         final ComputedCssStyleDeclaration parentStyle =
1831                 parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
1832         return parentStyle.getCalculatedWidth(false, false) - (getBorderHorizontal() + getPaddingHorizontal());
1833     }
1834 
1835     /**
1836      * Returns a fixed, hardcoded default width for element types that real
1837      * browsers give an intrinsic size to regardless of content -- currently
1838      * {@code <canvas>}, {@code <iframe>} and {@code <frame>}.
1839      *
1840      * @param element the element to check
1841      * @return the fixed default width in pixels, or {@code null} if
1842      *     {@code element} isn't one of the special-cased tag types
1843      */
1844     private static Integer getFixedDefaultWidth(final DomElement element) {
1845         if (element instanceof HtmlCanvas) {
1846             return 300;
1847         }
1848 
1849         // iframes have a default width of 300px, like canvas -- unless an
1850         // explicit 'width' HTML attribute (not CSS style) says otherwise
1851         if (element instanceof HtmlInlineFrame iframe) {
1852             final String widthAttribute = iframe.getAttributeDirect("width");
1853             if (DomElement.ATTRIBUTE_NOT_DEFINED != widthAttribute) {
1854                 return CssPixelValueConverter.pixelValue(widthAttribute);
1855             }
1856             return 300;
1857         }
1858 
1859         if (element instanceof HtmlFrame) {
1860             // a <frame> (inside a <frameset>) takes the full width of its
1861             // enclosing window
1862             return element.getPage().getEnclosingWindow().getInnerWidth();
1863         }
1864 
1865         return null;
1866     }
1867 
1868     /**
1869      * Returns a hardcoded default width for specific form-control tags that
1870      * real browsers size intrinsically based on their native widget rendering
1871      * rather than pure CSS content flow -- buttons (sized from their label
1872      * text), text/password inputs, checkboxes/radio buttons, textareas and
1873      * images.
1874      *
1875      * @param element the element to check
1876      * @return the default width in pixels, or {@code null} if {@code element}
1877      *     isn't one of the special-cased form-control types
1878      */
1879     private Integer getDefaultWidthForFormControl(final DomElement element) {
1880         final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
1881 
1882         if (element instanceof HtmlSubmitInput
1883                 || element instanceof HtmlResetInput
1884                 || element instanceof HtmlButtonInput
1885                 || element instanceof HtmlButton
1886                 || element instanceof HtmlFileInput) {
1887             // use asNormalizedText() here because getVisibleText() returns an
1888             // empty string for submit and reset buttons; default font for
1889             // buttons is a bit smaller than the body font size, hence the 0.9
1890             // scale factor
1891             final String text = element.asNormalizedText();
1892             return 10 + (int) (text.length() * browserVersion.getPixelsPerChar() * 0.9);
1893         }
1894 
1895         if (element instanceof HtmlTextInput || element instanceof HtmlPasswordInput) {
1896             // native text-input widget width varies by rendering engine/version
1897             if (browserVersion.hasFeature(JS_CLIENTWIDTH_INPUT_TEXT_173)) {
1898                 return 173;
1899             }
1900             if (browserVersion.hasFeature(JS_CLIENTWIDTH_INPUT_TEXT_157)) {
1901                 return 157;
1902             }
1903             return 161; // FF
1904         }
1905 
1906         if (element instanceof HtmlRadioButtonInput || element instanceof HtmlCheckBoxInput) {
1907             return browserVersion.hasFeature(JS_CLIENTWIDTH_RADIO_CHECKBOX_14) ? 14 : 13;
1908         }
1909 
1910         if (element instanceof HtmlTextArea) {
1911             return 100; // wild guess
1912         }
1913 
1914         if (element instanceof HtmlImage image) {
1915             return image.getWidthOrDefault();
1916         }
1917 
1918         return null;
1919     }
1920 
1921     /**
1922      * Returns the total width of the element's children.
1923      * @return the total width of the element's children
1924      */
1925     public int getContentWidth() {
1926         final int cachedContentWidth = getCachedContentWidth();
1927         if (cachedContentWidth != Integer.MIN_VALUE) {
1928             return cachedContentWidth;
1929         }
1930 
1931         int inlineWidth = 0;
1932         int maxBlockWidth = 0;
1933         final DomElement element = getDomElement();
1934         Iterable<DomNode> children = element.getChildren();
1935         if (element instanceof BaseFrameElement frameElement) {
1936             final Page enclosedPage = frameElement.getEnclosedPage();
1937             if (enclosedPage != null && enclosedPage.isHtmlPage()) {
1938                 children = ((DomNode) enclosedPage).getChildren();
1939             }
1940         }
1941         final WebWindow webWindow = element.getPage().getEnclosingWindow();
1942         for (final DomNode child : children) {
1943             if (child instanceof HtmlElement e) {
1944                 final ComputedCssStyleDeclaration style = webWindow.getComputedStyle(e, null);
1945                 final String childDisplay = style.getDisplay();
1946                 final int w;
1947                 if (BLOCK.equals(childDisplay)) {
1948                     // For block children, only use their full calculated width if they have
1949                     // an explicit width set. Otherwise recurse into their own content width
1950                     // so that shrink-wrap containers propagate the leaf width all the way up.
1951                     final String childStyleWidth = style.getStyleAttribute(Definition.WIDTH, true);
1952                     if (StringUtils.isEmptyOrNull(childStyleWidth) || AUTO.equals(childStyleWidth)) {
1953                         // No explicit width: the child's contribution is whatever its own
1954                         // content needs (recurse), plus its own border/padding.
1955                         w = style.getContentWidth()
1956                                 + style.getBorderHorizontal()
1957                                 + style.getPaddingHorizontal();
1958                     }
1959                     else {
1960                         w = style.getCalculatedWidth(true, true);
1961                     }
1962                     if (w > maxBlockWidth) {
1963                         maxBlockWidth = w;
1964                     }
1965                 }
1966                 else {
1967                     // Inline / inline-block children sit side-by-side; sum their widths.
1968                     w = style.getCalculatedWidth(true, true);
1969                     inlineWidth += w;
1970                 }
1971             }
1972             else if (child instanceof DomText) {
1973                 final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
1974                 final DomNode parent = child.getParentNode();
1975                 if (parent instanceof HtmlElement) {
1976                     final ComputedCssStyleDeclaration style = webWindow.getComputedStyle((DomElement) parent, null);
1977                     final int height = browserVersion.getFontHeight(
1978                                         style.getStyleAttribute(Definition.FONT_SIZE, true));
1979                     inlineWidth += child.getVisibleText().length() * (int) (height / 1.8f);
1980                 }
1981                 else {
1982                     inlineWidth += child.getVisibleText().length() * browserVersion.getPixelsPerChar();
1983                 }
1984             }
1985         }
1986 
1987         return updateCachedContentWidth(Math.max(maxBlockWidth, inlineWidth));
1988     }
1989 
1990     /**
1991      * Calculates the intrinsic or base text-line height of an element
1992      * without performing full child-element flow layout.
1993      *
1994      * @return the element's calculated height
1995      */
1996     private int calculateIntrinsicHeight(final DomElement element) {
1997         final int cachedEmptyHeight = getCachedEmptyHeight();
1998         if (cachedEmptyHeight != Integer.MIN_VALUE) {
1999             return cachedEmptyHeight;
2000         }
2001 
2002         if (!element.mayBeDisplayed()) {
2003             return updateCachedEmptyHeight(0);
2004         }
2005 
2006         final String display = getDisplay();
2007         if (NONE.equals(display)) {
2008             return updateCachedEmptyHeight(0);
2009         }
2010 
2011         final SgmlPage page = element.getPage();
2012         final WebWindow webWindow = page.getEnclosingWindow();
2013         final int windowHeight = webWindow.getInnerHeight();
2014 
2015         if (element instanceof HtmlBody) {
2016             if (page instanceof HtmlPage htmlPage && htmlPage.isQuirksMode()) {
2017                 return updateCachedEmptyHeight(windowHeight);
2018             }
2019 
2020             return updateCachedEmptyHeight(0);
2021         }
2022 
2023         int defaultHeight;
2024         if (ZERO_HEIGHT_ELEMENT_CLASSES.contains(element.getClass()) && StringUtils.isBlank(element.getTextContent())) {
2025             defaultHeight = 0;
2026         }
2027         else if (element.getFirstChild() == null) {
2028             if (element instanceof HtmlRadioButtonInput || element instanceof HtmlCheckBoxInput) {
2029                 final BrowserVersion browser = webWindow.getWebClient().getBrowserVersion();
2030                 if (browser.hasFeature(JS_CLIENTHEIGHT_RADIO_CHECKBOX_14)) {
2031                     defaultHeight = 14;
2032                 }
2033                 else {
2034                     defaultHeight = 13;
2035                 }
2036             }
2037             else if (element instanceof HtmlButton) {
2038                 defaultHeight = 20;
2039             }
2040             else if (element instanceof HtmlInput && !(element instanceof HtmlHiddenInput)) {
2041                 final BrowserVersion browser = webWindow.getWebClient().getBrowserVersion();
2042                 if (browser.hasFeature(JS_CLIENTHEIGHT_INPUT_17)) {
2043                     defaultHeight = 17;
2044                 }
2045                 else if (browser.hasFeature(JS_CLIENTHEIGHT_INPUT_18)) {
2046                     defaultHeight = 18;
2047                 }
2048                 else {
2049                     defaultHeight = 20;
2050                 }
2051             }
2052             else if (element instanceof HtmlSelect) {
2053                 defaultHeight = 20;
2054             }
2055             else if (element instanceof HtmlTextArea) {
2056                 defaultHeight = 49;
2057             }
2058             else {
2059                 defaultHeight = 0;
2060             }
2061         }
2062         else if (element instanceof HtmlRb) {
2063             final BrowserVersion browser = webWindow.getWebClient().getBrowserVersion();
2064             if (browser.hasFeature(JS_CLIENTHEIGHT_RB_17)) {
2065                 defaultHeight = 17;
2066             }
2067             else {
2068                 defaultHeight = 0;
2069             }
2070         }
2071         else if (element instanceof HtmlRt) {
2072             final BrowserVersion browser = webWindow.getWebClient().getBrowserVersion();
2073             if (browser.hasFeature(JS_CLIENTHEIGHT_RT_9)) {
2074                 defaultHeight = 9;
2075             }
2076             else {
2077                 defaultHeight = 0;
2078             }
2079         }
2080         else if (element instanceof HtmlRuby) {
2081             final BrowserVersion browser = webWindow.getWebClient().getBrowserVersion();
2082             if (browser.hasFeature(JS_CLIENTHEIGHT_RUBY_17)) {
2083                 defaultHeight = 17;
2084             }
2085             else {
2086                 defaultHeight = 0;
2087             }
2088         }
2089         else {
2090             final String headingDefault = HEADING_DEFAULT_FONT_SIZES.get(element.getClass());
2091             final boolean isHeading = headingDefault != null;
2092 
2093             final String fontSize;
2094             if (isHeading) {
2095                 final String value = getStyleAttribute(Definition.FONT_SIZE, false);
2096                 fontSize = value.isEmpty() ? headingDefault : getStyleAttribute(Definition.FONT_SIZE, true);
2097             }
2098             else {
2099                 fontSize = getStyleAttribute(Definition.FONT_SIZE, true);
2100             }
2101 
2102             defaultHeight = webWindow.getWebClient().getBrowserVersion().getFontHeight(fontSize);
2103 
2104             if (isHeading
2105                     || element instanceof HtmlDivision
2106                     || element instanceof HtmlSpan) {
2107                 String width = getStyleAttribute(Definition.WIDTH, false);
2108 
2109                 // maybe we are enclosed something that forces a width
2110                 DomNode parent = getDomElement().getParentNode();
2111                 final WebWindow win = parent.getPage().getEnclosingWindow();
2112                 while (width.isEmpty() && parent != null) {
2113                     if (parent instanceof DomElement domElement) {
2114                         final ComputedCssStyleDeclaration computedCss = win.getComputedStyle(domElement, null);
2115                         width = computedCss.getStyleAttribute(Definition.WIDTH, false);
2116                     }
2117                     parent = parent.getParentNode();
2118                     if (parent instanceof Page) {
2119                         break;
2120                     }
2121                 }
2122                 final int pixelWidth = CssPixelValueConverter.pixelValue(width);
2123                 final String content = element.getVisibleText();
2124 
2125                 if (pixelWidth > 0
2126                         && !width.isEmpty()
2127                         && StringUtils.isNotBlank(content)) {
2128                     final int fontSizeInt = CssPixelValueConverter.pixelValue(fontSize);
2129                     final int lineCount = Platform.getFontUtil().countLines(content, pixelWidth, fontSizeInt);
2130                     defaultHeight *= lineCount;
2131                 }
2132                 else {
2133                     if (element instanceof HtmlSpan && StringUtils.isEmptyOrNull(content)) {
2134                         defaultHeight = 0;
2135                     }
2136                     else {
2137                         defaultHeight *= org.apache.commons.lang3.StringUtils.countMatches(content, '\n') + 1;
2138                     }
2139                 }
2140 
2141                 final String styleHeight = getStyleAttribute(Definition.HEIGHT, true);
2142                 if (styleHeight.endsWith("%")) {
2143                     if (page instanceof HtmlPage htmlPage && !htmlPage.isQuirksMode()) {
2144                         return updateCachedEmptyHeight(defaultHeight);
2145                     }
2146                 }
2147             }
2148         }
2149 
2150         if (element instanceof HtmlInlineFrame iframe) {
2151             final String heightAttribute = iframe.getAttributeDirect("height");
2152             if (DomElement.ATTRIBUTE_NOT_DEFINED != heightAttribute) {
2153                 final int height = CssPixelValueConverter.pixelValue(heightAttribute);
2154                 return updateCachedEmptyHeight(height);
2155             }
2156 
2157             defaultHeight = 154;
2158         }
2159 
2160         final boolean isInline = INLINE.equals(display) && !(element instanceof HtmlInlineFrame);
2161 
2162         final int defaultHght = defaultHeight;
2163         final int defaultWindowHeight = element instanceof HtmlCanvas ? 150 : windowHeight;
2164 
2165         int height = CssPixelValueConverter.pixelValue(element,
2166                 style -> {
2167                     final DomElement elem = style.getDomElement();
2168                     if (elem instanceof HtmlBody) {
2169                         return String.valueOf(elem.getPage().getEnclosingWindow().getInnerHeight());
2170                     }
2171                     // height is ignored for inline elements
2172                     if (isInline) {
2173                         return "";
2174                     }
2175                     return style.getStyleAttribute(Definition.HEIGHT, true);
2176                 },
2177                 () -> defaultHght,
2178                 () -> defaultWindowHeight);
2179 
2180         if (height == 0 && (isInline || super.getHeight().isEmpty())) {
2181             height = defaultHeight;
2182         }
2183 
2184         return updateCachedEmptyHeight(height);
2185     }
2186 
2187     /**
2188      * Returns the total height of the element's children taking positioning into account.
2189      *
2190      * @return the total height of the element's children in pixels
2191      */
2192     public int getContentHeight() {
2193         // There are two different kinds of elements that might determine the content height:
2194         //  - elements with position:static or position:relative (elements that flow and build on each other)
2195         //  - elements with position:absolute (independent elements)
2196 
2197         final DomNode node = getDomElement();
2198         if (!node.mayBeDisplayed()) {
2199             return 0;
2200         }
2201 
2202         // Pre-size set to reduce reallocation overhead during tree scanning
2203         final Set<ComputedCssStyleDeclaration> styles = new HashSet<>(8);
2204 
2205         if (node instanceof HtmlTableRow row) {
2206             for (final HtmlTableCell cell : row.getCellIterator()) {
2207                 if (cell.mayBeDisplayed()) {
2208                     final ComputedCssStyleDeclaration style =
2209                             cell.getPage().getEnclosingWindow().getComputedStyle(cell, null);
2210                     styles.add(style);
2211                 }
2212             }
2213         }
2214         else {
2215             ComputedCssStyleDeclaration lastFlowing = null;
2216             for (final DomNode child : node.getChildren()) {
2217                 if (child.mayBeDisplayed() && child instanceof HtmlElement e) {
2218                     final ComputedCssStyleDeclaration style =
2219                             e.getPage().getEnclosingWindow().getComputedStyle(e, null);
2220                     final String position = style.getPositionWithInheritance();
2221                     if (STATIC.equals(position) || RELATIVE.equals(position)) {
2222                         lastFlowing = style;
2223                     }
2224                     else if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
2225                         styles.add(style);
2226                     }
2227                 }
2228             }
2229 
2230             if (lastFlowing != null) {
2231                 styles.add(lastFlowing);
2232             }
2233         }
2234 
2235         int max = 0;
2236         for (final ComputedCssStyleDeclaration style : styles) {
2237             final int h = style.getTop(true, false, false) + style.getCalculatedHeight(true, true);
2238             if (h > max) {
2239                 max = h;
2240             }
2241         }
2242         return max;
2243     }
2244 
2245     /**
2246      * Returns {@code true} if the element is scrollable along the specified axis.
2247      * @param horizontal if {@code true}, the caller is interested in scrollability along the x-axis;
2248      *        if {@code false}, the caller is interested in scrollability along the y-axis
2249      * @return {@code true} if the element is scrollable along the specified axis
2250      */
2251     public boolean isScrollable(final boolean horizontal) {
2252         return isScrollable(getDomElement(), horizontal, false);
2253     }
2254 
2255     /**
2256      * Returns true if this element shows a scrollbar.
2257      *
2258      * @param ignoreSize whether to consider the content/calculated width/height
2259      * @return true if this element shows a scrollbar
2260      */
2261     private boolean isScrollable(final DomElement element, final boolean horizontal, final boolean ignoreSize) {
2262         final boolean scrollable;
2263 
2264         String overflow;
2265         if (horizontal) {
2266             overflow = getStyleAttribute(Definition.OVERFLOW_X_, false);
2267             if (StringUtils.isEmptyOrNull(overflow)) {
2268                 overflow = getStyleAttribute(Definition.OVERFLOW_X, false);
2269             }
2270             // fall back to default
2271             if (StringUtils.isEmptyOrNull(overflow)) {
2272                 overflow = getStyleAttribute(Definition.OVERFLOW, true);
2273             }
2274             scrollable = (element instanceof HtmlBody || SCROLL.equals(overflow) || AUTO.equals(overflow))
2275                 && (ignoreSize || getContentWidth() > getCalculatedWidth(element));
2276         }
2277         else {
2278             overflow = getStyleAttribute(Definition.OVERFLOW_Y_, false);
2279             if (StringUtils.isEmptyOrNull(overflow)) {
2280                 overflow = getStyleAttribute(Definition.OVERFLOW_Y, false);
2281             }
2282             // fall back to default
2283             if (StringUtils.isEmptyOrNull(overflow)) {
2284                 overflow = getStyleAttribute(Definition.OVERFLOW, true);
2285             }
2286 
2287             scrollable = (element instanceof HtmlBody || SCROLL.equals(overflow) || AUTO.equals(overflow))
2288                 && (ignoreSize || getContentHeight() > calculateIntrinsicHeight(element));
2289         }
2290         return scrollable;
2291     }
2292 
2293     private int getBorderHorizontal() {
2294         final int borderHorizontal = getCachedBorderHorizontal();
2295         if (borderHorizontal != Integer.MIN_VALUE) {
2296             return borderHorizontal;
2297         }
2298 
2299         final int border = NONE.equals(getDisplay()) ? 0 : getBorderLeftValue() + getBorderRightValue();
2300         return updateCachedBorderHorizontal(border);
2301     }
2302 
2303     private int getBorderVertical() {
2304         final int borderVertical = getCachedBorderVertical();
2305         if (borderVertical != Integer.MIN_VALUE) {
2306             return borderVertical;
2307         }
2308 
2309         final int border = NONE.equals(getDisplay()) ? 0 : getBorderTopValue() + getBorderBottomValue();
2310         return updateCachedBorderVertical(border);
2311     }
2312 
2313     /**
2314      * Gets the size of the left border of the element.
2315      * @return the value in pixels
2316      */
2317     public int getBorderLeftValue() {
2318         return CssPixelValueConverter.pixelValue(getBorderLeftWidth());
2319     }
2320 
2321     /**
2322      * Gets the size of the right border of the element.
2323      * @return the value in pixels
2324      */
2325     public int getBorderRightValue() {
2326         return CssPixelValueConverter.pixelValue(getBorderRightWidth());
2327     }
2328 
2329     /**
2330      * Gets the size of the top border of the element.
2331      * @return the value in pixels
2332      */
2333     public int getBorderTopValue() {
2334         return CssPixelValueConverter.pixelValue(getBorderTopWidth());
2335     }
2336 
2337     /**
2338      * Gets the size of the bottom border of the element.
2339      * @return the value in pixels
2340      */
2341     public int getBorderBottomValue() {
2342         return CssPixelValueConverter.pixelValue(getBorderBottomWidth());
2343     }
2344 
2345     private int getPaddingHorizontal() {
2346         final int paddingHorizontal = getCachedPaddingHorizontal();
2347         if (paddingHorizontal != Integer.MIN_VALUE) {
2348             return paddingHorizontal;
2349         }
2350 
2351         final int padding = NONE.equals(getDisplay()) ? 0 : getPaddingLeftValue() + getPaddingRightValue();
2352         return updateCachedPaddingHorizontal(padding);
2353     }
2354 
2355     private int getPaddingVertical() {
2356         final int paddingVertical = getCachedPaddingVertical();
2357         if (paddingVertical != Integer.MIN_VALUE) {
2358             return paddingVertical;
2359         }
2360 
2361         final int padding = NONE.equals(getDisplay()) ? 0 : getPaddingTopValue() + getPaddingBottomValue();
2362         return updateCachedPaddingVertical(padding);
2363     }
2364 
2365     /**
2366      * Gets the left padding of the element.
2367      * @return the value in pixels
2368      */
2369     public int getPaddingLeftValue() {
2370         return CssPixelValueConverter.pixelValue(getPaddingLeft());
2371     }
2372 
2373     /**
2374      * Gets the right padding of the element.
2375      * @return the value in pixels
2376      */
2377     public int getPaddingRightValue() {
2378         return CssPixelValueConverter.pixelValue(getPaddingRight());
2379     }
2380 
2381     /**
2382      * Gets the top padding of the element.
2383      * @return the value in pixels
2384      */
2385     public int getPaddingTopValue() {
2386         return CssPixelValueConverter.pixelValue(getPaddingTop());
2387     }
2388 
2389     /**
2390      * Gets the bottom padding of the element.
2391      * @return the value in pixels
2392      */
2393     public int getPaddingBottomValue() {
2394         return CssPixelValueConverter.pixelValue(getPaddingBottom());
2395     }
2396 
2397     /**
2398      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2399      * @return the cached width
2400      */
2401     public int getCachedWidth() {
2402         return cachedWidth_;
2403     }
2404 
2405     /**
2406      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2407      * @param width the new value
2408      * @return the param width
2409      */
2410     public int updateCachedWidth(final int width) {
2411         cachedWidth_ = width;
2412         return cachedWidth_;
2413     }
2414 
2415     /**
2416      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2417      * @return the cached content width
2418      */
2419     public int getCachedContentWidth() {
2420         return cachedContentWidth_;
2421     }
2422 
2423     /**
2424      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2425      * @param contentWidth the new value
2426      * @return the contentWidth
2427      */
2428     public int updateCachedContentWidth(final int contentWidth) {
2429         cachedContentWidth_ = contentWidth;
2430         return cachedContentWidth_;
2431     }
2432 
2433     /**
2434      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2435      * @return the cached height
2436      */
2437     public int getCachedHeight() {
2438         return cachedHeight_;
2439     }
2440 
2441     /**
2442      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2443      * @param height the new value
2444      * @return the param height
2445      */
2446     public int updateCachedHeight(final int height) {
2447         cachedHeight_ = height;
2448         return cachedHeight_;
2449     }
2450 
2451     /**
2452      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2453      * @return the cached emptyHeight
2454      */
2455     public int getCachedEmptyHeight() {
2456         return cachedEmptyHeight_;
2457     }
2458 
2459     /**
2460      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2461      * @param emptyHeight the new value
2462      * @return the param emptyHeight
2463      */
2464     public int updateCachedEmptyHeight(final int emptyHeight) {
2465         cachedEmptyHeight_ = emptyHeight;
2466         return cachedEmptyHeight_;
2467     }
2468 
2469     /**
2470      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2471      * @return the cached top
2472      */
2473     public int getCachedTop() {
2474         return cachedTop_;
2475     }
2476 
2477     /**
2478      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2479      * @param top the new value
2480      */
2481     public void setCachedTop(final int top) {
2482         cachedTop_ = top;
2483     }
2484 
2485     /**
2486      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2487      * @return the cached padding horizontal
2488      */
2489     public int getCachedPaddingHorizontal() {
2490         return cachedPaddingHorizontal_;
2491     }
2492 
2493     /**
2494      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2495      * @param paddingHorizontal the new value
2496      * @return the param paddingHorizontal
2497      */
2498     public int updateCachedPaddingHorizontal(final int paddingHorizontal) {
2499         cachedPaddingHorizontal_ = paddingHorizontal;
2500         return cachedPaddingHorizontal_;
2501     }
2502 
2503     /**
2504      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2505      * @return the cached padding vertical
2506      */
2507     public int getCachedPaddingVertical() {
2508         return cachedPaddingVertical_;
2509     }
2510 
2511     /**
2512      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2513      * @param paddingVertical the new value
2514      * @return the param paddingVertical
2515      */
2516     public int updateCachedPaddingVertical(final int paddingVertical) {
2517         cachedPaddingVertical_ = paddingVertical;
2518         return cachedPaddingVertical_;
2519     }
2520 
2521     /**
2522      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2523      * @return the cached border horizontal
2524      */
2525     public int getCachedBorderHorizontal() {
2526         return cachedBorderHorizontal_;
2527     }
2528 
2529     /**
2530      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2531      * @param borderHorizontal the new value
2532      * @return the borderHorizontal
2533      */
2534     public int updateCachedBorderHorizontal(final int borderHorizontal) {
2535         cachedBorderHorizontal_ = borderHorizontal;
2536         return cachedBorderHorizontal_;
2537     }
2538 
2539     /**
2540      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2541      * @return the cached border vertical
2542      */
2543     public int getCachedBorderVertical() {
2544         return cachedBorderVertical_;
2545     }
2546 
2547     /**
2548      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span>
2549      * @param borderVertical the new value
2550      * @return the param borderVertical
2551      */
2552     public int updateCachedBorderVertical(final int borderVertical) {
2553         cachedBorderVertical_ = borderVertical;
2554         return cachedBorderVertical_;
2555     }
2556 
2557     /**
2558      * Makes a local, "computed", modification to this CSS style.
2559      *
2560      * @param declaration the style declaration
2561      * @param selector the selector determining that the style applies to this element
2562      */
2563     public void applyStyleFromSelector(final CSSStyleDeclarationImpl declaration, final Selector selector) {
2564         final SelectorSpecificity specificity = selector.getSelectorSpecificity();
2565         for (final Property prop : declaration.getProperties()) {
2566             final String name = prop.getName();
2567             final String value = declaration.getPropertyValue(name);
2568             final String priority = declaration.getPropertyPriority(name);
2569             applyLocalStyleAttribute(name, value, priority, specificity);
2570         }
2571     }
2572 
2573     private void applyLocalStyleAttribute(final String name, final String newValue, final String priority,
2574             final SelectorSpecificity specificity) {
2575         if (!StyleElement.PRIORITY_IMPORTANT.equals(priority)) {
2576             final StyleElement existingElement = localModifications_.get(name);
2577             if (existingElement != null) {
2578                 if (existingElement.isImportant()) {
2579                     return; // can't override a !important rule by a normal rule. Ignore it!
2580                 }
2581                 else if (specificity.compareTo(existingElement.getSpecificity()) < 0) {
2582                     return; // can't override a rule with a rule having higher specificity
2583                 }
2584             }
2585         }
2586         final StyleElement element = new StyleElement(name, newValue, priority, specificity);
2587         localModifications_.put(name, element);
2588     }
2589 
2590     /**
2591      * Makes a local, "computed", modification to this CSS style that won't override other
2592      * style attributes of the same name. This method should be used to set default values
2593      * for style attributes.
2594      *
2595      * @param name the name of the style attribute to set
2596      * @param newValue the value of the style attribute to set
2597      */
2598     public void setDefaultLocalStyleAttribute(final String name, final String newValue) {
2599         final StyleElement element = new StyleElement(name, newValue, "", SelectorSpecificity.DEFAULT_STYLE_ATTRIBUTE);
2600         localModifications_.put(name, element);
2601     }
2602 
2603     /**
2604      * {@inheritDoc}
2605      */
2606     @Override
2607     public boolean hasFeature(final BrowserVersionFeatures property) {
2608         return getDomElement().hasFeature(property);
2609     }
2610 
2611     /**
2612      * {@inheritDoc}
2613      */
2614     @Override
2615     public BrowserVersion getBrowserVersion() {
2616         return getDomElement().getPage().getWebClient().getBrowserVersion();
2617     }
2618 
2619     /**
2620      * {@inheritDoc}
2621      */
2622     @Override
2623     public boolean isComputed() {
2624         return true;
2625     }
2626 
2627     /**
2628      * {@inheritDoc}
2629      */
2630     @Override
2631     public String toString() {
2632         return "ComputedCssStyleDeclaration for '" + getDomElement() + "'";
2633     }
2634 
2635     private String defaultIfEmpty(final String str, final StyleAttributes.Definition definition) {
2636         return defaultIfEmpty(str, definition, false);
2637     }
2638 
2639     private String defaultIfEmpty(final String str, final StyleAttributes.Definition definition,
2640             final boolean isPixel) {
2641         if (!getDomElement().isAttachedToPage()) {
2642             return EMPTY_FINAL;
2643         }
2644         if (str == null || str.isEmpty()) {
2645             return definition.getDefaultComputedValue(getBrowserVersion());
2646         }
2647         if (isPixel) {
2648             return pixelString(str);
2649         }
2650         return str;
2651     }
2652 
2653     /**
2654      * Returns the string, or {@code toReturnIfEmptyOrDefault}.
2655      *
2656      * @param toReturnIfEmptyOrDefault the value to return if empty or equals the {@code defaultValue}
2657      * @param defaultValue the default value of the string
2658      * @return the string, or {@code toReturnIfEmptyOrDefault}
2659      */
2660     private String defaultIfEmpty(final String str, final String toReturnIfEmptyOrDefault, final String defaultValue) {
2661         if (!getDomElement().isAttachedToPage()) {
2662             return EMPTY_FINAL;
2663         }
2664         if (str == null || str.isEmpty() || str.equals(defaultValue)) {
2665             return toReturnIfEmptyOrDefault;
2666         }
2667         return str;
2668     }
2669 
2670     /**
2671      * Returns the specified length value as a pixel length value.
2672      * This method does <b>NOT</b> handle percentages correctly;
2673      * use {@link CssPixelValueConverter#pixelValue(DomElement, CssValue)} if you need percentage support.
2674      * @param value the length value to convert to a pixel length value
2675      * @return the specified length value as a pixel length value
2676      * @see CssPixelValueConverter#pixelString(DomElement, CssValue)
2677      */
2678     private static String pixelString(final String value) {
2679         if (EMPTY_FINAL == value || value.endsWith("px")) {
2680             return value;
2681         }
2682         return CssPixelValueConverter.pixelValue(value) + "px";
2683     }
2684 }