All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.htmlunit.css.ComputedCssStyleDeclaration Maven / Gradle / Ivy

Go to download

XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.

There is a newer version: 8.4.1
Show newest version
/*
 * Copyright (c) 2002-2024 Gargoyle Software Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.htmlunit.css;

import static org.htmlunit.BrowserVersionFeatures.CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY;
import static org.htmlunit.BrowserVersionFeatures.CSS_STYLE_PROP_FONT_DISCONNECTED_IS_EMPTY;
import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_INPUT_17;
import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_INPUT_18;
import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTHEIGHT_RADIO_CHECKBOX_10;
import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTWIDTH_INPUT_TEXT_143;
import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTWIDTH_INPUT_TEXT_173;
import static org.htmlunit.BrowserVersionFeatures.JS_CLIENTWIDTH_RADIO_CHECKBOX_10;
import static org.htmlunit.css.CssStyleSheet.ABSOLUTE;
import static org.htmlunit.css.CssStyleSheet.AUTO;
import static org.htmlunit.css.CssStyleSheet.BLOCK;
import static org.htmlunit.css.CssStyleSheet.FIXED;
import static org.htmlunit.css.CssStyleSheet.INHERIT;
import static org.htmlunit.css.CssStyleSheet.INLINE;
import static org.htmlunit.css.CssStyleSheet.NONE;
import static org.htmlunit.css.CssStyleSheet.RELATIVE;
import static org.htmlunit.css.CssStyleSheet.SCROLL;
import static org.htmlunit.css.CssStyleSheet.STATIC;

import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.lang3.StringUtils;
import org.htmlunit.BrowserVersion;
import org.htmlunit.BrowserVersionFeatures;
import org.htmlunit.Page;
import org.htmlunit.WebWindow;
import org.htmlunit.css.StyleAttributes.Definition;
import org.htmlunit.cssparser.dom.AbstractCSSRuleImpl;
import org.htmlunit.cssparser.dom.CSSStyleDeclarationImpl;
import org.htmlunit.cssparser.dom.Property;
import org.htmlunit.cssparser.parser.selector.Selector;
import org.htmlunit.cssparser.parser.selector.SelectorSpecificity;
import org.htmlunit.html.BaseFrameElement;
import org.htmlunit.html.DomElement;
import org.htmlunit.html.DomNode;
import org.htmlunit.html.DomText;
import org.htmlunit.html.HtmlAbbreviated;
import org.htmlunit.html.HtmlAcronym;
import org.htmlunit.html.HtmlAddress;
import org.htmlunit.html.HtmlArticle;
import org.htmlunit.html.HtmlAside;
import org.htmlunit.html.HtmlBaseFont;
import org.htmlunit.html.HtmlBidirectionalIsolation;
import org.htmlunit.html.HtmlBidirectionalOverride;
import org.htmlunit.html.HtmlBig;
import org.htmlunit.html.HtmlBlink;
import org.htmlunit.html.HtmlBody;
import org.htmlunit.html.HtmlBold;
import org.htmlunit.html.HtmlButton;
import org.htmlunit.html.HtmlButtonInput;
import org.htmlunit.html.HtmlCanvas;
import org.htmlunit.html.HtmlCenter;
import org.htmlunit.html.HtmlCheckBoxInput;
import org.htmlunit.html.HtmlCitation;
import org.htmlunit.html.HtmlCode;
import org.htmlunit.html.HtmlData;
import org.htmlunit.html.HtmlDefinition;
import org.htmlunit.html.HtmlDefinitionDescription;
import org.htmlunit.html.HtmlDefinitionTerm;
import org.htmlunit.html.HtmlDivision;
import org.htmlunit.html.HtmlElement;
import org.htmlunit.html.HtmlEmphasis;
import org.htmlunit.html.HtmlFigure;
import org.htmlunit.html.HtmlFigureCaption;
import org.htmlunit.html.HtmlFileInput;
import org.htmlunit.html.HtmlFooter;
import org.htmlunit.html.HtmlHeader;
import org.htmlunit.html.HtmlHiddenInput;
import org.htmlunit.html.HtmlImage;
import org.htmlunit.html.HtmlInlineFrame;
import org.htmlunit.html.HtmlInput;
import org.htmlunit.html.HtmlItalic;
import org.htmlunit.html.HtmlKeyboard;
import org.htmlunit.html.HtmlLayer;
import org.htmlunit.html.HtmlLegend;
import org.htmlunit.html.HtmlMain;
import org.htmlunit.html.HtmlMark;
import org.htmlunit.html.HtmlNav;
import org.htmlunit.html.HtmlNoBreak;
import org.htmlunit.html.HtmlNoEmbed;
import org.htmlunit.html.HtmlNoFrames;
import org.htmlunit.html.HtmlNoLayer;
import org.htmlunit.html.HtmlNoScript;
import org.htmlunit.html.HtmlOutput;
import org.htmlunit.html.HtmlPasswordInput;
import org.htmlunit.html.HtmlPlainText;
import org.htmlunit.html.HtmlRadioButtonInput;
import org.htmlunit.html.HtmlRb;
import org.htmlunit.html.HtmlResetInput;
import org.htmlunit.html.HtmlRp;
import org.htmlunit.html.HtmlRt;
import org.htmlunit.html.HtmlRtc;
import org.htmlunit.html.HtmlRuby;
import org.htmlunit.html.HtmlS;
import org.htmlunit.html.HtmlSample;
import org.htmlunit.html.HtmlSection;
import org.htmlunit.html.HtmlSelect;
import org.htmlunit.html.HtmlSlot;
import org.htmlunit.html.HtmlSmall;
import org.htmlunit.html.HtmlSpan;
import org.htmlunit.html.HtmlStrike;
import org.htmlunit.html.HtmlStrong;
import org.htmlunit.html.HtmlSubmitInput;
import org.htmlunit.html.HtmlSubscript;
import org.htmlunit.html.HtmlSummary;
import org.htmlunit.html.HtmlSuperscript;
import org.htmlunit.html.HtmlTableCell;
import org.htmlunit.html.HtmlTableRow;
import org.htmlunit.html.HtmlTeletype;
import org.htmlunit.html.HtmlTextArea;
import org.htmlunit.html.HtmlTextInput;
import org.htmlunit.html.HtmlTime;
import org.htmlunit.html.HtmlUnderlined;
import org.htmlunit.html.HtmlUnknownElement;
import org.htmlunit.html.HtmlVariable;
import org.htmlunit.html.HtmlWordBreak;
import org.htmlunit.javascript.host.Element;
import org.htmlunit.javascript.host.html.HTMLElement;
import org.htmlunit.platform.Platform;

/**
 * An object for a CSSStyleDeclaration, which is computed.
 *
 * @see org.htmlunit.javascript.host.Window#getComputedStyle(Object, String)
 *
 * @author Ahmed Ashour
 * @author Marc Guillemot
 * @author Ronald Brill
 * @author Frank Danek
 * @author Alex Gorbatovsky
 * @author cd alexndr
 */
public class ComputedCssStyleDeclaration extends AbstractCssStyleDeclaration {

    /** The set of 'inheritable' definitions. */
    private static final Set INHERITABLE_DEFINITIONS = EnumSet.of(
        Definition.AZIMUTH,
        Definition.BORDER_COLLAPSE,
        Definition.BORDER_SPACING,
        Definition.CAPTION_SIDE,
        Definition.COLOR,
        Definition.CURSOR,
        Definition.DIRECTION,
        Definition.ELEVATION,
        Definition.EMPTY_CELLS,
        Definition.FONT_FAMILY,
        Definition.FONT_SIZE,
        Definition.FONT_STYLE,
        Definition.FONT_VARIANT,
        Definition.FONT_WEIGHT,
        Definition.FONT,
        Definition.LETTER_SPACING,
        Definition.LINE_HEIGHT,
        Definition.LIST_STYLE_IMAGE,
        Definition.LIST_STYLE_POSITION,
        Definition.LIST_STYLE_TYPE,
        Definition.LIST_STYLE,
        Definition.ORPHANS,
        Definition.PITCH_RANGE,
        Definition.PITCH,
        Definition.QUOTES,
        Definition.RICHNESS,
        Definition.SPEAK_HEADER,
        Definition.SPEAK_NUMERAL,
        Definition.SPEAK_PUNCTUATION,
        Definition.SPEAK,
        Definition.SPEECH_RATE,
        Definition.STRESS,
        Definition.TEXT_ALIGN,
        Definition.TEXT_INDENT,
        Definition.TEXT_TRANSFORM,
        Definition.VISIBILITY,
        Definition.VOICE_FAMILY,
        Definition.VOLUME,
        Definition.WHITE_SPACE,
        Definition.WIDOWS,
        Definition.WORD_SPACING);

    /** Denotes a value which should be returned as is. */
    public static final String EMPTY_FINAL = new String("");

    /** The computed, cached width of the element to which this computed style belongs (no padding, borders, etc.). */
    private Integer width_;

    /**
     * The computed, cached height of the element to which this computed style belongs (no padding, borders, etc.),
     * taking child elements into account.
     */
    private Integer height_;

    /**
     * The computed, cached height of the element to which this computed style belongs (no padding, borders, etc.),
     * not taking child elements into account.
     */
    private Integer height2_;

    /** The computed, cached horizontal padding (left + right) of the element to which this computed style belongs. */
    private Integer paddingHorizontal_;

    /** The computed, cached vertical padding (top + bottom) of the element to which this computed style belongs. */
    private Integer paddingVertical_;

    /** The computed, cached horizontal border (left + right) of the element to which this computed style belongs. */
    private Integer borderHorizontal_;

    /** The computed, cached vertical border (top + bottom) of the element to which this computed style belongs. */
    private Integer borderVertical_;

    /** The computed, cached top of the element to which this computed style belongs. */
    private Integer top_;

    /**
     * Local modifications maintained here rather than in the element. We use a sorted
     * map so that results are deterministic and thus easily testable.
     */
    private final SortedMap localModifications_ = new TreeMap<>();

    /** The wrapped CSSStyleDeclaration */
    private ElementCssStyleDeclaration elementStyleDeclaration_;

    public ComputedCssStyleDeclaration(final ElementCssStyleDeclaration styleDeclaration) {
        elementStyleDeclaration_ = styleDeclaration;
        getDomElement().setDefaults(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getStylePriority(final String name) {
        return elementStyleDeclaration_.getStylePriority(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCssText() {
        return elementStyleDeclaration_.getCssText();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getStyleAttribute(final String name) {
        final StyleElement element = getStyleElement(name);
        if (element != null && element.getValue() != null) {
            final String value = element.getValue();
            if (!"content".equals(name)
                    && !value.contains("url")) {
                return org.htmlunit.util.StringUtils.toRootLowerCase(value);
            }
            return value;
        }
        return "";
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getStyleAttribute(final Definition definition, final boolean getDefaultValueIfEmpty) {
        final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
        final boolean feature = hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY);
        final boolean isDefInheritable = INHERITABLE_DEFINITIONS.contains(definition);

        // to make the fuzzer happy the recursion was removed
        final ComputedCssStyleDeclaration[] queue = new ComputedCssStyleDeclaration[] {this};
        String value = null;
        while (queue[0] != null) {
            value = getStyleAttributeWorker(definition, getDefaultValueIfEmpty,
                        browserVersion, feature, isDefInheritable, queue);
        }

        return value;
    }

    private static String getStyleAttributeWorker(final Definition definition,
                final boolean getDefaultValueIfEmpty, final BrowserVersion browserVersion,
                final boolean feature, final boolean isDefInheritable,
                final ComputedCssStyleDeclaration[] queue) {
        final ComputedCssStyleDeclaration decl = queue[0];
        queue[0] = null;

        final DomElement domElem = decl.getDomElement();
        if (!domElem.isAttachedToPage() && feature) {
            return EMPTY_FINAL;
        }

        String value = decl.getStyleAttribute(definition.getAttributeName());
        if (value.isEmpty()) {
            final DomNode parent = domElem.getParentNode();
            if (isDefInheritable && parent instanceof DomElement) {
                final WebWindow window = domElem.getPage().getEnclosingWindow();

                queue[0] = window.getComputedStyle((DomElement) parent, null);
            }
            else if (getDefaultValueIfEmpty) {
                value = definition.getDefaultComputedValue(browserVersion);
            }
        }

        return value;
    }

    /**
     * @param toReturnIfEmptyOrDefault the value to return if empty or equals the {@code defaultValue}
     * @param defaultValue the default value of the string
     * @return the string, or {@code toReturnIfEmptyOrDefault}
     */
    private String getStyleAttribute(final Definition definition, final String toReturnIfEmptyOrDefault,
            final String defaultValue) {
        final DomElement domElement = getDomElement();

        if (!domElement.isAttachedToPage()
                && hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
            return ComputedCssStyleDeclaration.EMPTY_FINAL;
        }

        final boolean feature = hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY);
        final boolean isDefInheritable = INHERITABLE_DEFINITIONS.contains(definition);

        // to make the fuzzer happy the recursion was removed
        final BrowserVersion browserVersion = domElement.getPage().getWebClient().getBrowserVersion();
        final ComputedCssStyleDeclaration[] queue = new ComputedCssStyleDeclaration[] {this};
        String value = null;
        while (queue[0] != null) {
            value = getStyleAttributeWorker(definition, false,
                    browserVersion, feature, isDefInheritable, queue);
        }

        if (value == null || value.isEmpty() || value.equals(defaultValue)) {
            return toReturnIfEmptyOrDefault;
        }

        return value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setCssText(final String value) {
        // read only
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setStyleAttribute(final String name, final String newValue, final String important) {
        // read only
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String removeStyleAttribute(final String name) {
        // read only
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getLength() {
        return elementStyleDeclaration_.getLength();
    }

    /**
     * @return the width
     */
    @Override
    public String getWidth() {
        if (NONE.equals(getDisplay())) {
            return AUTO;
        }

        final DomElement domElem = getDomElement();
        if (!domElem.isAttachedToPage()) {
            if (hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
                return "";
            }
            if (getStyleAttribute(Definition.WIDTH, true).isEmpty()) {
                return AUTO;
            }
        }

        final int windowWidth = domElem.getPage().getEnclosingWindow().getInnerWidth();
        return CssPixelValueConverter.pixelString(domElem, new CssPixelValueConverter.CssValue(0, windowWidth) {
            @Override
            public String get(final ComputedCssStyleDeclaration style) {
                final String value = style.getStyleAttribute(Definition.WIDTH, true);
                if (StringUtils.isEmpty(value)) {
                    final String position = getStyleAttribute(Definition.POSITION, true);
                    if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
                        final String content = domElem.getVisibleText();
                        // do this only for small content
                        // at least for empty div's this is more correct
                        if (null != content && content.length() < 13) {
                            return (content.length() * 7) + "px";
                        }
                    }

                    int windowDefaultValue = getWindowDefaultValue();
                    if (domElem instanceof HtmlBody) {
                        windowDefaultValue -= 16;
                    }
                    return windowDefaultValue + "px";
                }
                else if (AUTO.equals(value)) {
                    int windowDefaultValue = getWindowDefaultValue();
                    if (domElem instanceof HtmlBody) {
                        windowDefaultValue -= 16;
                    }
                    return windowDefaultValue + "px";
                }

                return value;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object item(final int index) {
        return elementStyleDeclaration_.item(index);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AbstractCSSRuleImpl getParentRule() {
        return elementStyleDeclaration_.getParentRule();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StyleElement getStyleElement(final String name) {
        final StyleElement existent = elementStyleDeclaration_.getStyleElement(name);

        if (localModifications_ != null) {
            final StyleElement localStyleMod = localModifications_.get(name);
            if (localStyleMod == null) {
                return existent;
            }

            if (existent == null) {
                // Local modifications represent either default style elements or style elements
                // defined in stylesheets; either way, they shouldn't overwrite any style
                // elements derived directly from the HTML element's "style" attribute.
                return localStyleMod;
            }

            // replace if !IMPORTANT
            if (StyleElement.PRIORITY_IMPORTANT.equals(localStyleMod.getPriority())) {
                if (existent.isImportant()) {
                    if (existent.getSpecificity().compareTo(localStyleMod.getSpecificity()) < 0) {
                        return localStyleMod;
                    }
                }
                else {
                    return localStyleMod;
                }
            }
        }
        return existent;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StyleElement getStyleElementCaseInSensitive(final String name) {
        return elementStyleDeclaration_.getStyleElementCaseInSensitive(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map getStyleMap() {
        return elementStyleDeclaration_.getStyleMap();
    }

    public DomElement getDomElement() {
        return elementStyleDeclaration_.getDomElement();
    }

    /**
     * @return the accelerator setting
     */
    @Override
    public String getAccelerator() {
        return getStyleAttribute(Definition.ACCELERATOR, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBackgroundAttachment() {
        return defaultIfEmpty(super.getBackgroundAttachment(), Definition.BACKGROUND_ATTACHMENT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBackgroundColor() {
        final String value = super.getBackgroundColor();
        if (StringUtils.isEmpty(value)) {
            return Definition.BACKGROUND_COLOR.getDefaultComputedValue(getBrowserVersion());
        }
        return CssColors.toRGBColor(value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBackgroundImage() {
        return defaultIfEmpty(super.getBackgroundImage(), Definition.BACKGROUND_IMAGE);
    }

    /**
     * Gets the {@code backgroundPosition} style attribute.
     * @return the style attribute
     */
    @Override
    public String getBackgroundPosition() {
        return defaultIfEmpty(super.getBackgroundPosition(), Definition.BACKGROUND_POSITION);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBackgroundRepeat() {
        return defaultIfEmpty(super.getBackgroundRepeat(), Definition.BACKGROUND_REPEAT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderBottomColor() {
        return defaultIfEmpty(super.getBorderBottomColor(), Definition.BORDER_BOTTOM_COLOR);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderBottomStyle() {
        return defaultIfEmpty(super.getBorderBottomStyle(), Definition.BORDER_BOTTOM_STYLE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderBottomWidth() {
        return pixelString(defaultIfEmpty(super.getBorderBottomWidth(), Definition.BORDER_BOTTOM_WIDTH));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderLeftColor() {
        return defaultIfEmpty(super.getBorderLeftColor(), Definition.BORDER_LEFT_COLOR);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderLeftStyle() {
        return defaultIfEmpty(super.getBorderLeftStyle(), Definition.BORDER_LEFT_STYLE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderLeftWidth() {
        return pixelString(defaultIfEmpty(super.getBorderLeftWidth(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderRightColor() {
        return defaultIfEmpty(super.getBorderRightColor(), "rgb(0, 0, 0)", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderRightStyle() {
        return defaultIfEmpty(super.getBorderRightStyle(), NONE, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderRightWidth() {
        return pixelString(defaultIfEmpty(super.getBorderRightWidth(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderTopColor() {
        return defaultIfEmpty(super.getBorderTopColor(), "rgb(0, 0, 0)", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderTopStyle() {
        return defaultIfEmpty(super.getBorderTopStyle(), NONE, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getBorderTopWidth() {
        return pixelString(defaultIfEmpty(super.getBorderTopWidth(), "0px", null));
    }

    /**
     * @return the bottom setting
     */
    @Override
    public String getBottom() {
        return getStyleAttribute(Definition.BOTTOM, AUTO, null);
    }

    /**
     * @return the color setting
     */
    @Override
    public String getColor() {
        final String value = getStyleAttribute(Definition.COLOR, "rgb(0, 0, 0)", null);
        return CssColors.toRGBColor(value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCssFloat() {
        return defaultIfEmpty(super.getCssFloat(), Definition.CSS_FLOAT);
    }

    /**
     * @return the display setting
     */
    @Override
    public String getDisplay() {
        final DomElement domElem = getDomElement();
        if (!domElem.isAttachedToPage()) {
            if (hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
                return "";
            }
        }

        // don't use defaultIfEmpty for performance
        // (no need to calculate the default if not empty)
        final String value = getStyleAttribute(Definition.DISPLAY.getAttributeName());
        if (StringUtils.isEmpty(value)) {
            if (domElem instanceof HtmlElement) {
                return ((HtmlElement) domElem).getDefaultStyleDisplay().value();
            }
            return "";
        }
        return value;
    }

    /**
     * @return the font setting
     */
    @Override
    public String getFont() {
        final DomElement domElem = getDomElement();
        if (domElem.isAttachedToPage()) {
            if (hasFeature(CSS_STYLE_PROP_FONT_DISCONNECTED_IS_EMPTY)) {
                return getStyleAttribute(Definition.FONT, true);
            }
        }
        return "";
    }

    /**
     * @return the font family setting
     */
    @Override
    public String getFontFamily() {
        return getStyleAttribute(Definition.FONT_FAMILY, true);
    }

    /**
     * @return the font size setting
     */
    @Override
    public String getFontSize() {
        String value = getStyleAttribute(Definition.FONT_SIZE, true);
        if (!value.isEmpty()) {
            value = CssPixelValueConverter.pixelValue(value) + "px";
        }
        return value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getLineHeight() {
        return defaultIfEmpty(super.getLineHeight(), Definition.LINE_HEIGHT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getHeight() {
        if (NONE.equals(getDisplay())) {
            return AUTO;
        }

        final DomElement elem = getDomElement();
        if (!elem.isAttachedToPage()) {
            if (hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
                return "";
            }
            if (getStyleAttribute(Definition.HEIGHT, true).isEmpty()) {
                return AUTO;
            }
        }
        final int windowHeight = elem.getPage().getEnclosingWindow().getInnerHeight();
        return CssPixelValueConverter
                .pixelString(elem, new CssPixelValueConverter.CssValue(0, windowHeight) {
                    @Override
                    public String get(final ComputedCssStyleDeclaration style) {
                        // TODO don't reach out to the js peer
                        final String offsetHeight = ((HTMLElement) elem.getScriptableObject()).getOffsetHeight() + "px";
                        return defaultIfEmpty(style.getStyleAttribute(Definition.HEIGHT, true), offsetHeight, AUTO);
                    }
                });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getLeft() {
        final String superLeft = super.getLeft();
        if (!superLeft.endsWith("%")) {
            return defaultIfEmpty(superLeft, AUTO, null);
        }

        final DomElement element = getDomElement();
        return CssPixelValueConverter.pixelString(element, new CssPixelValueConverter.CssValue(0, 0) {
            @Override
            public String get(final ComputedCssStyleDeclaration style) {
                if (style.getDomElement() == element) {
                    return style.getStyleAttribute(Definition.LEFT, true);
                }
                return style.getStyleAttribute(Definition.WIDTH, true);
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getLetterSpacing() {
        return defaultIfEmpty(super.getLetterSpacing(), "normal", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMargin() {
        return defaultIfEmpty(super.getMargin(), Definition.MARGIN, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMarginBottom() {
        return pixelString(defaultIfEmpty(super.getMarginBottom(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMarginLeft() {
        return getMarginX(super.getMarginLeft(), Definition.MARGIN_LEFT);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMarginRight() {
        return getMarginX(super.getMarginRight(), Definition.MARGIN_RIGHT);
    }

    private String getMarginX(final String superMarginX, final Definition definition) {
        if (!superMarginX.endsWith("%")) {
            return pixelString(defaultIfEmpty(superMarginX, "0px", null));
        }
        final DomElement element = getDomElement();
        if (!element.isAttachedToPage() && hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
            return "";
        }

        final int windowWidth = element.getPage().getEnclosingWindow().getInnerWidth();
        return CssPixelValueConverter
                .pixelString(element, new CssPixelValueConverter.CssValue(0, windowWidth) {
                    @Override
                    public String get(final ComputedCssStyleDeclaration style) {
                        if (style.getDomElement() == element) {
                            return style.getStyleAttribute(definition, true);
                        }
                        return style.getStyleAttribute(Definition.WIDTH, true);
                    }
                });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMarginTop() {
        return pixelString(defaultIfEmpty(super.getMarginTop(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMaxHeight() {
        return defaultIfEmpty(super.getMaxHeight(), NONE, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMaxWidth() {
        return defaultIfEmpty(super.getMaxWidth(), NONE, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMinHeight() {
        return defaultIfEmpty(super.getMinHeight(), "0px", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMinWidth() {
        return defaultIfEmpty(super.getMinWidth(), "0px", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getOpacity() {
        return defaultIfEmpty(super.getOpacity(), "1", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getOrphans() {
        return defaultIfEmpty(super.getOrphans(), Definition.ORPHANS);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getOutlineWidth() {
        return defaultIfEmpty(super.getOutlineWidth(), "0px", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPadding() {
        return defaultIfEmpty(super.getPadding(), Definition.PADDING, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPaddingBottom() {
        return pixelString(defaultIfEmpty(super.getPaddingBottom(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPaddingLeft() {
        return pixelString(defaultIfEmpty(super.getPaddingLeft(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPaddingRight() {
        return pixelString(defaultIfEmpty(super.getPaddingRight(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPaddingTop() {
        return pixelString(defaultIfEmpty(super.getPaddingTop(), "0px", null));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getRight() {
        return defaultIfEmpty(super.getRight(), AUTO, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTextIndent() {
        return defaultIfEmpty(super.getTextIndent(), "0px", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTop() {
        final DomElement element = getDomElement();
        if (!element.isAttachedToPage() && hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
            return "";
        }
        final String superTop = super.getTop();
        if (!superTop.endsWith("%")) {
            return defaultIfEmpty(superTop, Definition.TOP);
        }

        return CssPixelValueConverter.pixelString(element, new CssPixelValueConverter.CssValue(0, 0) {
            @Override
            public String get(final ComputedCssStyleDeclaration style) {
                if (style.getDomElement() == element) {
                    return style.getStyleAttribute(Definition.TOP, true);
                }
                return style.getStyleAttribute(Definition.HEIGHT, true);
            }
        });
    }

    /**
     * Returns the computed top (Y coordinate), relative to the node's parent's top edge.
     * @param includeMargin whether or not to take the margin into account in the calculation
     * @param includeBorder whether or not to take the border into account in the calculation
     * @param includePadding whether or not to take the padding into account in the calculation
     * @return the computed top (Y coordinate), relative to the node's parent's top edge
     */
    public int getTop(final boolean includeMargin, final boolean includeBorder, final boolean includePadding) {
        Integer cachedTop = getCachedTop();

        int top = 0;
        if (null == cachedTop) {
            final String position = getPositionWithInheritance();
            if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
                top = getTopForAbsolutePositionWithInheritance();
            }
            else if (getDomElement() instanceof HtmlTableCell) {
                top = 0;
            }
            else {
                // Calculate the vertical displacement caused by *previous* siblings.
                DomNode prev = getDomElement().getPreviousSibling();
                boolean prevHadComputedTop = false;
                while (prev != null && !prevHadComputedTop) {
                    if (prev instanceof HtmlElement) {
                        final ComputedCssStyleDeclaration style =
                                prev.getPage().getEnclosingWindow().getComputedStyle((DomElement) prev, null);

                        // only previous block elements are counting
                        final String display = style.getDisplay();
                        if (isBlock(display)) {
                            int prevTop = 0;
                            final Integer eCachedTop = style.getCachedTop();
                            if (eCachedTop == null) {
                                final String prevPosition = style.getPositionWithInheritance();
                                if (ABSOLUTE.equals(prevPosition) || FIXED.equals(prevPosition)) {
                                    prevTop += style.getTopForAbsolutePositionWithInheritance();
                                }
                                else {
                                    if (RELATIVE.equals(prevPosition)) {
                                        final String t = style.getTopWithInheritance();
                                        prevTop += CssPixelValueConverter.pixelValue(t);
                                    }
                                }
                            }
                            else {
                                prevHadComputedTop = true;
                                prevTop += eCachedTop.intValue();
                            }
                            prevTop += style.getCalculatedHeight(true, true);
                            final int margin = CssPixelValueConverter.pixelValue(style.getMarginTop());
                            prevTop += margin;
                            top += prevTop;
                        }
                    }
                    prev = prev.getPreviousSibling();
                }
                // If the position is relative, we also need to add the specified "top" displacement.
                if (RELATIVE.equals(position)) {
                    final String t = getTopWithInheritance();
                    top += CssPixelValueConverter.pixelValue(t);
                }
            }
            cachedTop = Integer.valueOf(top);
            setCachedTop(cachedTop);
        }
        else {
            top = cachedTop.intValue();
        }

        if (includeMargin) {
            final int margin = CssPixelValueConverter.pixelValue(getMarginTop());
            top += margin;
        }

        if (includeBorder) {
            final int border = CssPixelValueConverter.pixelValue(getBorderTopWidth());
            top += border;
        }

        if (includePadding) {
            final int padding = getPaddingTopValue();
            top += padding;
        }

        return top;
    }

    private static boolean isBlock(final String display) {
        return display != null
                && !INLINE.equals(display)
                && !NONE.equals(display);
    }

    /**
     * Returns the CSS {@code top} attribute, replacing inherited values with the actual parent values.
     * @return the CSS {@code top} attribute, replacing inherited values with the actual parent values
     */
    public String getTopWithInheritance() {
        String top = getTop();
        if (INHERIT.equals(top)) {
            final HtmlElement parent = (HtmlElement) getDomElement().getParentNode();
            if (parent == null) {
                top = AUTO;
            }
            else {
                final ComputedCssStyleDeclaration style =
                        parent.getPage().getEnclosingWindow().getComputedStyle(parent, null);
                top = style.getTopWithInheritance();
            }
        }
        return top;
    }

    /**
     * Returns the CSS {@code bottom} attribute, replacing inherited values with the actual parent values.
     * @return the CSS {@code bottom} attribute, replacing inherited values with the actual parent values
     */
    public String getBottomWithInheritance() {
        String bottom = getBottom();
        if (INHERIT.equals(bottom)) {
            final DomNode parent = getDomElement().getParentNode();
            if (parent == null) {
                bottom = AUTO;
            }
            else {
                final ComputedCssStyleDeclaration style =
                        parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
                bottom = style.getBottomWithInheritance();
            }
        }
        return bottom;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getVerticalAlign() {
        return defaultIfEmpty(super.getVerticalAlign(), "baseline", null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getWidows() {
        return defaultIfEmpty(super.getWidows(), Definition.WIDOWS);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getWordSpacing() {
        return defaultIfEmpty(super.getWordSpacing(), Definition.WORD_SPACING);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getZIndex() {
        final Object response = super.getZIndex();
        if (response.toString().isEmpty()) {
            return AUTO;
        }
        return response;
    }

    /**
     * Gets the left margin of the element.
     * @return the value in pixels
     */
    public int getMarginLeftValue() {
        return CssPixelValueConverter.pixelValue(getMarginLeft());
    }

    /**
     * Gets the right margin of the element.
     * @return the value in pixels
     */
    public int getMarginRightValue() {
        return CssPixelValueConverter.pixelValue(getMarginRight());
    }

    /**
     * Gets the top margin of the element.
     * @return the value in pixels
     */
    public int getMarginTopValue() {
        return CssPixelValueConverter.pixelValue(getMarginTop());
    }

    /**
     * Gets the bottom margin of the element.
     * @return the value in pixels
     */
    public int getMarginBottomValue() {
        return CssPixelValueConverter.pixelValue(getMarginBottom());
    }

    /**
     * Returns the computed left (X coordinate), relative to the node's parent's left edge.
     * @param includeMargin whether or not to take the margin into account in the calculation
     * @param includeBorder whether or not to take the border into account in the calculation
     * @param includePadding whether or not to take the padding into account in the calculation
     * @return the computed left (X coordinate), relative to the node's parent's left edge
     */
    public int getLeft(final boolean includeMargin, final boolean includeBorder, final boolean includePadding) {
        final String p = getPositionWithInheritance();
        final String l = getLeftWithInheritance();
        final String r = getRightWithInheritance();

        int left;
        if ((ABSOLUTE.equals(p) || FIXED.equals(p)) && !AUTO.equals(l)) {
            // No need to calculate displacement caused by sibling nodes.
            left = CssPixelValueConverter.pixelValue(l);
        }
        else if ((ABSOLUTE.equals(p) || FIXED.equals(p)) && !AUTO.equals(r)) {
            // Need to calculate the horizontal displacement caused by *all* siblings.
            final DomNode parent = getDomElement().getParentNode();
            final int parentWidth;
            if (parent == null) {
                parentWidth = getDomElement().getPage().getEnclosingWindow().getInnerWidth();
            }
            else if (parent instanceof Page) {
                parentWidth = (((Page) parent).getEnclosingWindow()).getInnerWidth();
            }
            else {
                final ComputedCssStyleDeclaration parentStyle =
                        parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
                parentWidth = parentStyle.getCalculatedWidth(false, false);
            }
            left = parentWidth - CssPixelValueConverter.pixelValue(r);
        }
        else if (FIXED.equals(p) && !AUTO.equals(r)) {
            final DomElement e = getDomElement();
            final WebWindow win = e.getPage().getEnclosingWindow();
            final ComputedCssStyleDeclaration style = win.getComputedStyle(e, null);

            final DomNode parent = e.getParentNode();
            final int parentWidth;
            if (parent == null) {
                parentWidth = win.getInnerWidth();
            }
            else {
                final ComputedCssStyleDeclaration parentStyle = win.getComputedStyle((DomElement) parent, null);
                parentWidth = CssPixelValueConverter.pixelValue(parentStyle.getWidth())
                                - CssPixelValueConverter.pixelValue(style.getWidth());
            }
            left = parentWidth - CssPixelValueConverter.pixelValue(r);
        }
        else if (FIXED.equals(p) && AUTO.equals(l)) {
            // Fixed to the location at which the browser puts it via normal element flowing.
            final DomNode parent = getDomElement().getParentNode();
            if (parent == null || parent instanceof Page) {
                left = 0;
            }
            else {
                final ComputedCssStyleDeclaration style =
                        parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
                left = CssPixelValueConverter.pixelValue(style.getLeftWithInheritance());
            }
        }
        else if (STATIC.equals(p)) {
            // We need to calculate the horizontal displacement caused by *previous* siblings.
            left = 0;
            DomNode prev = getDomElement().getPreviousSibling();
            while (prev != null) {
                if (prev instanceof HtmlElement) {
                    final ComputedCssStyleDeclaration style =
                            prev.getPage().getEnclosingWindow().getComputedStyle((DomElement) prev, null);
                    final String d = style.getDisplay();
                    if (isBlock(d)) {
                        break;
                    }
                    else if (!NONE.equals(d)) {
                        left += style.getCalculatedWidth(true, true);
                    }
                }
                else if (prev instanceof DomText) {
                    final String content = prev.getVisibleText();
                    if (content != null) {
                        left += content.trim().length()
                                * getDomElement().getPage().getWebClient().getBrowserVersion().getPixesPerChar();
                    }
                }
                prev = prev.getPreviousSibling();
            }
        }
        else {
            // Just use the CSS specified value.
            left = CssPixelValueConverter.pixelValue(l);
        }

        if (includeMargin) {
            final int margin = getMarginLeftValue();
            left += margin;
        }

        if (includeBorder) {
            final int border = CssPixelValueConverter.pixelValue(getBorderLeftWidth());
            left += border;
        }

        if (includePadding) {
            final int padding = getPaddingLeftValue();
            left += padding;
        }

        return left;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPosition() {
        return defaultIfEmpty(super.getPosition(), Definition.POSITION);
    }

    /**
     * Returns the CSS {@code position} attribute, replacing inherited values with the actual parent values.
     * @return the CSS {@code position} attribute, replacing inherited values with the actual parent values
     */
    public String getPositionWithInheritance() {
        String p = getStyleAttribute(Definition.POSITION, true);
        if (INHERIT.equals(p)) {
            final DomNode parent = getDomElement().getParentNode();
            if (parent == null) {
                p = STATIC;
            }
            else {
                final ComputedCssStyleDeclaration style =
                        parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
                p = style.getPositionWithInheritance();
            }
        }
        return p;
    }

    /**
     * Returns the CSS {@code left} attribute, replacing inherited values with the actual parent values.
     * @return the CSS {@code left} attribute, replacing inherited values with the actual parent values
     */
    public String getLeftWithInheritance() {
        String left = getLeft();
        if (INHERIT.equals(left)) {
            final DomNode parent = getDomElement().getParentNode();
            if (parent == null) {
                left = AUTO;
            }
            else {
                final ComputedCssStyleDeclaration style =
                        parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
                left = style.getLeftWithInheritance();
            }
        }
        return left;
    }

    /**
     * Returns the CSS {@code right} attribute, replacing inherited values with the actual parent values.
     * @return the CSS {@code right} attribute, replacing inherited values with the actual parent values
     */
    public String getRightWithInheritance() {
        String right = getRight();
        if (INHERIT.equals(right)) {
            final DomNode parent = getDomElement().getParentNode();
            if (parent == null) {
                right = AUTO;
            }
            else {
                final ComputedCssStyleDeclaration style =
                        parent.getPage().getEnclosingWindow().getComputedStyle((DomElement) parent, null);
                right = style.getRightWithInheritance();
            }
        }
        return right;
    }

    private int getTopForAbsolutePositionWithInheritance() {
        final String t = getTopWithInheritance();

        if (!AUTO.equals(t)) {
            // No need to calculate displacement caused by sibling nodes.
            return CssPixelValueConverter.pixelValue(t);
        }

        final String b = getBottomWithInheritance();
        if (!AUTO.equals(b)) {
            // Estimate the vertical displacement caused by *all* siblings.
            // This is very rough, and doesn't even take position or display types into account.
            // It also doesn't take into account the fact that the parent's height may be hardcoded in CSS.
            int top = 0;
            DomNode child = getDomElement().getParentNode().getFirstChild();
            while (child != null) {
                if (child instanceof HtmlElement && child.mayBeDisplayed()) {
                    top += 20;
                }
                child = child.getNextSibling();
            }
            top -= CssPixelValueConverter.pixelValue(b);
            return top;
        }

        return 0;
    }

    /**
     * Returns the element's height, possibly including its padding and border.
     * @param includeBorder whether or not to include the border height in the returned value
     * @param includePadding whether or not to include the padding height in the returned value
     * @return the element's height, possibly including its padding and border
     */
    public int getCalculatedHeight(final boolean includeBorder, final boolean includePadding) {
        final DomElement element = getDomElement();

        if (!element.isAttachedToPage()) {
            return 0;
        }
        int height = getCalculatedHeight();
        if (!"border-box".equals(getStyleAttribute(Definition.BOX_SIZING, true))) {
            if (includeBorder) {
                height += getBorderVertical();
            }
            else if (isScrollable(false, true) && !(element instanceof HtmlBody)) {
                height -= 17;
            }

            if (includePadding) {
                height += getPaddingVertical();
            }
        }
        return height;
    }

    /**
     * Returns the element's calculated height, taking both relevant CSS and the element's children into account.
     * @return the element's calculated height, taking both relevant CSS and the element's children into account
     */
    private int getCalculatedHeight() {
        final Integer cachedHeight = getCachedHeight();
        if (cachedHeight != null) {
            return cachedHeight.intValue();
        }

        final DomElement element = getDomElement();

        if (element instanceof HtmlImage) {
            return setCachedHeight(((HtmlImage) element).getHeightOrDefault());
        }

        final boolean isInline = INLINE.equals(getDisplay()) && !(element instanceof HtmlInlineFrame);
        // height is ignored for inline elements
        if (isInline || super.getHeight().isEmpty()) {
            final int contentHeight = getContentHeight();
            if (contentHeight > 0) {
                return setCachedHeight(contentHeight);
            }
        }

        return setCachedHeight(getEmptyHeight());
    }

    /**
     * Returns the element's width in pixels, possibly including its padding and border.
     * @param includeBorder whether or not to include the border width in the returned value
     * @param includePadding whether or not to include the padding width in the returned value
     * @return the element's width in pixels, possibly including its padding and border
     */
    public int getCalculatedWidth(final boolean includeBorder, final boolean includePadding) {
        final DomElement element = getDomElement();

        if (!element.isAttachedToPage()) {
            return 0;
        }
        int width = getCalculatedWidth();
        if (!"border-box".equals(getStyleAttribute(Definition.BOX_SIZING, true))) {
            if (includeBorder) {
                width += getBorderHorizontal();
            }
            else if (isScrollable(true, true) && !(element instanceof HtmlBody)) {
                width -= 17;
            }

            if (includePadding) {
                width += getPaddingHorizontal();
            }
        }
        return width;
    }

    private int getCalculatedWidth() {
        final Integer cachedWidth = getCachedWidth();
        if (cachedWidth != null) {
            return cachedWidth.intValue();
        }

        final DomElement element = getDomElement();
        if (!element.mayBeDisplayed()) {
            return setCachedWidth(0);
        }

        final String display = getDisplay();
        if (NONE.equals(display)) {
            return setCachedWidth(0);
        }

        final int width;
        final String styleWidth = getStyleAttribute(Definition.WIDTH, true);
        final DomNode parent = element.getParentNode();

        // width is ignored for inline elements
        if ((INLINE.equals(display) || StringUtils.isEmpty(styleWidth)) && parent instanceof HtmlElement) {
            // hack: TODO find a way to specify default values for different tags
            if (element instanceof HtmlCanvas) {
                return 300;
            }

            // Width not explicitly set.
            final String cssFloat = getCssFloat();
            final String position = getStyleAttribute(Definition.POSITION, true);
            if ("right".equals(cssFloat) || "left".equals(cssFloat)
                    || ABSOLUTE.equals(position) || FIXED.equals(position)) {
                final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
                // We're floating; simplistic approximation: text content * pixels per character.
                width = element.getVisibleText().length() * browserVersion.getPixesPerChar();
            }
            else if (BLOCK.equals(display)) {
                final int windowWidth = element.getPage().getEnclosingWindow().getInnerWidth();
                if (element instanceof HtmlBody) {
                    width = windowWidth - 16;
                }
                else {
                    // Block elements take up 100% of the parent's width.
                    width = CssPixelValueConverter.pixelValue((DomElement) parent,
                                        new CssPixelValueConverter.CssValue(0, windowWidth) {
                            @Override public String get(final ComputedCssStyleDeclaration style) {
                                return style.getWidth();
                            }
                        }) - (getBorderHorizontal() + getPaddingHorizontal());
                }
            }
            else if (element instanceof HtmlSubmitInput
                        || element instanceof HtmlResetInput
                        || element instanceof HtmlButtonInput
                        || element instanceof HtmlButton
                        || element instanceof HtmlFileInput) {
                // use asNormalizedText() here because getVisibleText() returns an empty string
                // for submit and reset buttons
                final String text = element.asNormalizedText();
                final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
                // default font for buttons is a bit smaller than the body font size
                width = 10 + (int) (text.length() * browserVersion.getPixesPerChar() * 0.9);
            }
            else if (element instanceof HtmlTextInput || element instanceof HtmlPasswordInput) {
                final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
                if (browserVersion.hasFeature(JS_CLIENTWIDTH_INPUT_TEXT_143)) {
                    return 143;
                }
                if (browserVersion.hasFeature(JS_CLIENTWIDTH_INPUT_TEXT_173)) {
                    return 173;
                }
                width = 154; // FF
            }
            else if (element instanceof HtmlRadioButtonInput || element instanceof HtmlCheckBoxInput) {
                final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();
                if (browserVersion.hasFeature(JS_CLIENTWIDTH_RADIO_CHECKBOX_10)) {
                    width = 10;
                }
                else {
                    width = 13;
                }
            }
            else if (element instanceof HtmlTextArea) {
                width = 100; // wild guess
            }
            else if (element instanceof HtmlImage) {
                width = ((HtmlImage) element).getWidthOrDefault();
            }
            else {
                // Inline elements take up however much space is required by their children.
                width = getContentWidth();
            }
        }
        else if (AUTO.equals(styleWidth)) {
            width = element.getPage().getEnclosingWindow().getInnerWidth();
        }
        else {
            // Width explicitly set in the style attribute, or there was no parent to provide guidance.
            width = CssPixelValueConverter.pixelValue(element,
                    new CssPixelValueConverter.CssValue(0, element.getPage().getEnclosingWindow().getInnerWidth()) {
                    @Override public String get(final ComputedCssStyleDeclaration style) {
                        return style.getStyleAttribute(Definition.WIDTH, true);
                    }
                });
        }

        return setCachedWidth(width);
    }

    /**
     * Returns the total width of the element's children.
     * @return the total width of the element's children
     */
    public int getContentWidth() {
        int width = 0;
        final DomElement element = getDomElement();
        Iterable children = element.getChildren();
        if (element instanceof BaseFrameElement) {
            final Page enclosedPage = ((BaseFrameElement) element).getEnclosedPage();
            if (enclosedPage != null && enclosedPage.isHtmlPage()) {
                children = ((DomNode) enclosedPage).getChildren();
            }
        }
        final WebWindow webWindow = element.getPage().getEnclosingWindow();
        for (final DomNode child : children) {
            if (child instanceof HtmlElement) {
                final HtmlElement e = (HtmlElement) child;
                final ComputedCssStyleDeclaration style = webWindow.getComputedStyle(e, null);
                final int w = style.getCalculatedWidth(true, true);
                width += w;
            }
            else if (child instanceof DomText) {
                final BrowserVersion browserVersion = getDomElement().getPage().getWebClient().getBrowserVersion();

                final DomNode parent = child.getParentNode();
                if (parent instanceof HtmlElement) {
                    final ComputedCssStyleDeclaration style = webWindow.getComputedStyle((DomElement) parent, null);
                    final int height = browserVersion.getFontHeight(style.getFontSize());
                    width += child.getVisibleText().length() * (int) (height / 1.8f);
                }
                else {
                    width += child.getVisibleText().length() * browserVersion.getPixesPerChar();
                }
            }
        }
        return width;
    }

    /**
     * Returns the element's calculated height taking relevant CSS into account, but not the element's child
     * elements.
     *
     * @return the element's calculated height taking relevant CSS into account, but not the element's child
     *         elements
     */
    private int getEmptyHeight() {
        final Integer cachedHeight2 = getCachedHeight2();
        if (cachedHeight2 != null) {
            return cachedHeight2.intValue();
        }

        final DomElement element = getDomElement();
        if (!element.mayBeDisplayed()) {
            return setCachedHeight2(0);
        }

        final String display = getDisplay();
        if (NONE.equals(display)) {
            return setCachedHeight2(0);
        }

        final WebWindow webWindow = element.getPage().getEnclosingWindow();
        final int windowHeight = webWindow.getInnerHeight();

        if (element instanceof HtmlBody) {
            return setCachedHeight2(windowHeight);
        }

        final boolean isInline = INLINE.equals(display) && !(element instanceof HtmlInlineFrame);
        // height is ignored for inline elements
        final boolean explicitHeightSpecified = !isInline && !super.getHeight().isEmpty();

        int defaultHeight;
        if ((element instanceof HtmlAbbreviated
                || element instanceof HtmlAcronym
                || element instanceof HtmlAddress
                || element instanceof HtmlArticle
                || element instanceof HtmlAside
                || element instanceof HtmlBaseFont
                || element instanceof HtmlBidirectionalIsolation
                || element instanceof HtmlBidirectionalOverride
                || element instanceof HtmlBig
                || element instanceof HtmlBlink
                || element instanceof HtmlBold
                || element instanceof HtmlCenter
                || element instanceof HtmlCitation
                || element instanceof HtmlCode
                || element instanceof HtmlDefinition
                || element instanceof HtmlDefinitionDescription
                || element instanceof HtmlDefinitionTerm
                || element instanceof HtmlEmphasis
                || element instanceof HtmlFigure
                || element instanceof HtmlFigureCaption
                || element instanceof HtmlFooter
                || element instanceof HtmlHeader
                || element instanceof HtmlItalic
                || element instanceof HtmlKeyboard
                || element instanceof HtmlLayer
                || element instanceof HtmlMark
                || element instanceof HtmlNav
                || element instanceof HtmlNoBreak
                || element instanceof HtmlNoEmbed
                || element instanceof HtmlNoFrames
                || element instanceof HtmlNoLayer
                || element instanceof HtmlNoScript
                || element instanceof HtmlPlainText
                || element instanceof HtmlRuby
                || element instanceof HtmlRb
                || element instanceof HtmlRp
                || element instanceof HtmlRt
                || element instanceof HtmlRtc
                || element instanceof HtmlS
                || element instanceof HtmlSample
                || element instanceof HtmlSection
                || element instanceof HtmlSmall
                || element instanceof HtmlStrike
                || element instanceof HtmlStrong
                || element instanceof HtmlSubscript
                || element instanceof HtmlSummary
                || element instanceof HtmlSuperscript
                || element instanceof HtmlTeletype
                || element instanceof HtmlUnderlined
                || element instanceof HtmlUnknownElement
                || element instanceof HtmlWordBreak
                || element instanceof HtmlMain
                || element instanceof HtmlVariable

                || element instanceof HtmlDivision
                || element instanceof HtmlUnknownElement
                || element instanceof HtmlData
                || element instanceof HtmlTime
                || element instanceof HtmlOutput
                || element instanceof HtmlSlot
                || element instanceof HtmlLegend)
                && StringUtils.isBlank(element.getTextContent())) {
            defaultHeight = 0;
        }
        else if (element.getFirstChild() == null) {
            if (element instanceof HtmlRadioButtonInput || element instanceof HtmlCheckBoxInput) {
                if (webWindow.getWebClient().getBrowserVersion().hasFeature(JS_CLIENTHEIGHT_RADIO_CHECKBOX_10)) {
                    defaultHeight = 10;
                }
                else {
                    defaultHeight = 13;
                }
            }
            else if (element instanceof HtmlButton) {
                defaultHeight = 20;
            }
            else if (element instanceof HtmlInput && !(element instanceof HtmlHiddenInput)) {
                final BrowserVersion browser = webWindow.getWebClient().getBrowserVersion();
                if (browser.hasFeature(JS_CLIENTHEIGHT_INPUT_17)) {
                    defaultHeight = 17;
                }
                else if (browser.hasFeature(JS_CLIENTHEIGHT_INPUT_18)) {
                    defaultHeight = 18;
                }
                else {
                    defaultHeight = 20;
                }
            }
            else if (element instanceof HtmlSelect) {
                defaultHeight = 20;
            }
            else if (element instanceof HtmlTextArea) {
                defaultHeight = 49;
            }
            else if (element instanceof HtmlInlineFrame) {
                defaultHeight = 154;
            }
            else {
                defaultHeight = 0;
            }
        }
        else {
            final String fontSize = getFontSize();
            defaultHeight = webWindow.getWebClient().getBrowserVersion().getFontHeight(fontSize);

            if (element instanceof HtmlDivision
                    || element instanceof HtmlSpan) {
                String width = getStyleAttribute(Definition.WIDTH, false);

                // maybe we are enclosed something that forces a width
                DomNode parent = getDomElement().getParentNode();
                final WebWindow win = parent.getPage().getEnclosingWindow();
                while (width.isEmpty() && parent != null) {
                    if (parent instanceof DomElement) {
                        final ComputedCssStyleDeclaration computedCss = win.getComputedStyle((DomElement) parent, null);
                        width = computedCss.getStyleAttribute(Definition.WIDTH, false);
                    }
                    parent = parent.getParentNode();
                    if (parent instanceof Page) {
                        break;
                    }
                }
                final int pixelWidth = CssPixelValueConverter.pixelValue(width);
                final String content = element.getVisibleText();

                if (pixelWidth > 0
                        && !width.isEmpty()
                        && StringUtils.isNotBlank(content)) {
                    final int lineCount = Platform.getFontUtil().countLines(content, pixelWidth, fontSize);
                    defaultHeight *= lineCount;
                }
                else {
                    if (element instanceof HtmlSpan && StringUtils.isEmpty(content)) {
                        defaultHeight = 0;
                    }
                    else {
                        defaultHeight *= StringUtils.countMatches(content, '\n') + 1;
                    }
                }
            }
        }

        final int defaultWindowHeight = element instanceof HtmlCanvas ? 150 : windowHeight;

        int height = CssPixelValueConverter.pixelValue(element,
                new CssPixelValueConverter.CssValue(defaultHeight, defaultWindowHeight) {
                @Override public String get(final ComputedCssStyleDeclaration style) {
                    final DomElement elem = style.getDomElement();
                    if (elem instanceof HtmlBody) {
                        return String.valueOf(elem.getPage().getEnclosingWindow().getInnerHeight());
                    }
                    // height is ignored for inline elements
                    if (isInline) {
                        return "";
                    }
                    return style.getStyleAttribute(Definition.HEIGHT, true);
                }
            });

        if (height == 0 && !explicitHeightSpecified) {
            height = defaultHeight;
        }

        return setCachedHeight2(height);
    }

    /**
     * Returns the total height of the element's children.
     * @return the total height of the element's children
     */
    public int getContentHeight() {
        // There are two different kinds of elements that might determine the content height:
        //  - elements with position:static or position:relative (elements that flow and build on each other)
        //  - elements with position:absolute (independent elements)

        final DomNode node = getDomElement();
        if (!node.mayBeDisplayed()) {
            return 0;
        }

        ComputedCssStyleDeclaration lastFlowing = null;
        final Set styles = new HashSet<>();

        if (node instanceof HtmlTableRow) {
            final HtmlTableRow row = (HtmlTableRow) node;
            for (final HtmlTableCell cell : row.getCellIterator()) {
                if (cell.mayBeDisplayed()) {
                    final ComputedCssStyleDeclaration style =
                            cell.getPage().getEnclosingWindow().getComputedStyle(cell, null);
                    styles.add(style);
                }
            }
        }
        else {
            for (final DomNode child : node.getChildren()) {
                if (child.mayBeDisplayed()) {
                    if (child instanceof HtmlElement) {
                        final HtmlElement e = (HtmlElement) child;
                        final ComputedCssStyleDeclaration style =
                                e.getPage().getEnclosingWindow().getComputedStyle(e, null);
                        final String position = style.getPositionWithInheritance();
                        if (STATIC.equals(position) || RELATIVE.equals(position)) {
                            lastFlowing = style;
                        }
                        else if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
                            styles.add(style);
                        }
                    }
                }
            }

            if (lastFlowing != null) {
                styles.add(lastFlowing);
            }
        }

        int max = 0;
        for (final ComputedCssStyleDeclaration style : styles) {
            final int h = style.getTop(true, false, false) + style.getCalculatedHeight(true, true);
            if (h > max) {
                max = h;
            }
        }
        return max;
    }

    /**
     * Returns {@code true} if the element is scrollable along the specified axis.
     * @param horizontal if {@code true}, the caller is interested in scrollability along the x-axis;
     *        if {@code false}, the caller is interested in scrollability along the y-axis
     * @return {@code true} if the element is scrollable along the specified axis
     */
    public boolean isScrollable(final boolean horizontal) {
        return isScrollable(horizontal, false);
    }

    /**
     * @param ignoreSize whether to consider the content/calculated width/height
     */
    private boolean isScrollable(final boolean horizontal, final boolean ignoreSize) {
        final boolean scrollable;
        final DomElement element = getDomElement();
        final String overflow = getStyleAttribute(Definition.OVERFLOW, true);
        if (horizontal) {
            // TODO: inherit, overflow-x
            scrollable = (element instanceof HtmlBody || SCROLL.equals(overflow) || AUTO.equals(overflow))
                && (ignoreSize || getContentWidth() > getCalculatedWidth());
        }
        else {
            // TODO: inherit, overflow-y
            scrollable = (element instanceof HtmlBody || SCROLL.equals(overflow) || AUTO.equals(overflow))
                && (ignoreSize || getContentHeight() > getEmptyHeight());
        }
        return scrollable;
    }

    private int getBorderHorizontal() {
        final Integer borderHorizontal = getCachedBorderHorizontal();
        if (borderHorizontal != null) {
            return borderHorizontal.intValue();
        }

        final int border = NONE.equals(getDisplay()) ? 0 : getBorderLeftValue() + getBorderRightValue();
        return setCachedBorderHorizontal(border);
    }

    private int getBorderVertical() {
        final Integer borderVertical = getCachedBorderVertical();
        if (borderVertical != null) {
            return borderVertical.intValue();
        }

        final int border = NONE.equals(getDisplay()) ? 0 : getBorderTopValue() + getBorderBottomValue();
        return setCachedBorderVertical(border);
    }

    /**
     * Gets the size of the left border of the element.
     * @return the value in pixels
     */
    public int getBorderLeftValue() {
        return CssPixelValueConverter.pixelValue(getBorderLeftWidth());
    }

    /**
     * Gets the size of the right border of the element.
     * @return the value in pixels
     */
    public int getBorderRightValue() {
        return CssPixelValueConverter.pixelValue(getBorderRightWidth());
    }

    /**
     * Gets the size of the top border of the element.
     * @return the value in pixels
     */
    public int getBorderTopValue() {
        return CssPixelValueConverter.pixelValue(getBorderTopWidth());
    }

    /**
     * Gets the size of the bottom border of the element.
     * @return the value in pixels
     */
    public int getBorderBottomValue() {
        return CssPixelValueConverter.pixelValue(getBorderBottomWidth());
    }

    private int getPaddingHorizontal() {
        final Integer paddingHorizontal = getCachedPaddingHorizontal();
        if (paddingHorizontal != null) {
            return paddingHorizontal.intValue();
        }

        final int padding = NONE.equals(getDisplay()) ? 0 : getPaddingLeftValue() + getPaddingRightValue();
        return setCachedPaddingHorizontal(padding);
    }

    private int getPaddingVertical() {
        final Integer paddingVertical = getCachedPaddingVertical();
        if (paddingVertical != null) {
            return paddingVertical.intValue();
        }

        final int padding = NONE.equals(getDisplay()) ? 0 : getPaddingTopValue() + getPaddingBottomValue();
        return setCachedPaddingVertical(padding);
    }

    /**
     * Gets the left padding of the element.
     * @return the value in pixels
     */
    public int getPaddingLeftValue() {
        return CssPixelValueConverter.pixelValue(getPaddingLeft());
    }

    /**
     * Gets the right padding of the element.
     * @return the value in pixels
     */
    public int getPaddingRightValue() {
        return CssPixelValueConverter.pixelValue(getPaddingRight());
    }

    /**
     * Gets the top padding of the element.
     * @return the value in pixels
     */
    public int getPaddingTopValue() {
        return CssPixelValueConverter.pixelValue(getPaddingTop());
    }

    /**
     * Gets the bottom padding of the element.
     * @return the value in pixels
     */
    public int getPaddingBottomValue() {
        return CssPixelValueConverter.pixelValue(getPaddingBottom());
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached width
     */
    public Integer getCachedWidth() {
        return width_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param width the new value
     * @return the param width
     */
    public int setCachedWidth(final int width) {
        width_ = Integer.valueOf(width);
        return width;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached height
     */
    public Integer getCachedHeight() {
        return height_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param height the new value
     * @return the param height
     */
    public int setCachedHeight(final int height) {
        height_ = Integer.valueOf(height);
        return height;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached height2
     */
    public Integer getCachedHeight2() {
        return height2_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param height the new value
     * @return the param height2
     */
    public int setCachedHeight2(final int height) {
        height2_ = Integer.valueOf(height);
        return height;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached top
     */
    public Integer getCachedTop() {
        return top_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param top the new value
     */
    public void setCachedTop(final Integer top) {
        top_ = top;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached padding horizontal
     */
    public Integer getCachedPaddingHorizontal() {
        return paddingHorizontal_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param paddingHorizontal the new value
     * @return the param paddingHorizontal
     */
    public int setCachedPaddingHorizontal(final int paddingHorizontal) {
        paddingHorizontal_ = Integer.valueOf(paddingHorizontal);
        return paddingHorizontal;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached padding vertical
     */
    public Integer getCachedPaddingVertical() {
        return paddingVertical_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param paddingVertical the new value
     * @return the param paddingVertical
     */
    public int setCachedPaddingVertical(final int paddingVertical) {
        paddingVertical_ = Integer.valueOf(paddingVertical);
        return paddingVertical;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached border horizontal
     */
    public Integer getCachedBorderHorizontal() {
        return borderHorizontal_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param borderHorizontal the new value
     * @return the param borderHorizontal
     */
    public int setCachedBorderHorizontal(final int borderHorizontal) {
        borderHorizontal_ = Integer.valueOf(borderHorizontal);
        return borderHorizontal;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @return the cached border vertical
     */
    public Integer getCachedBorderVertical() {
        return borderVertical_;
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
     * @param borderVertical the new value
     * @return the param borderVertical
     */
    public int setCachedBorderVertical(final int borderVertical) {
        borderVertical_ = Integer.valueOf(borderVertical);
        return borderVertical;
    }

    /**
     * Makes a local, "computed", modification to this CSS style.
     *
     * @param declaration the style declaration
     * @param selector the selector determining that the style applies to this element
     */
    public void applyStyleFromSelector(final CSSStyleDeclarationImpl declaration, final Selector selector) {
        final SelectorSpecificity specificity = selector.getSelectorSpecificity();
        for (final Property prop : declaration.getProperties()) {
            final String name = prop.getName();
            final String value = declaration.getPropertyValue(name);
            final String priority = declaration.getPropertyPriority(name);
            applyLocalStyleAttribute(name, value, priority, specificity);
        }
    }

    private void applyLocalStyleAttribute(final String name, final String newValue, final String priority,
            final SelectorSpecificity specificity) {
        if (!StyleElement.PRIORITY_IMPORTANT.equals(priority)) {
            final StyleElement existingElement = localModifications_.get(name);
            if (existingElement != null) {
                if (existingElement.isImportant()) {
                    return; // can't override a !important rule by a normal rule. Ignore it!
                }
                else if (specificity.compareTo(existingElement.getSpecificity()) < 0) {
                    return; // can't override a rule with a rule having higher specificity
                }
            }
        }
        final StyleElement element = new StyleElement(name, newValue, priority, specificity);
        localModifications_.put(name, element);
    }

    /**
     * Makes a local, "computed", modification to this CSS style that won't override other
     * style attributes of the same name. This method should be used to set default values
     * for style attributes.
     *
     * @param name the name of the style attribute to set
     * @param newValue the value of the style attribute to set
     */
    public void setDefaultLocalStyleAttribute(final String name, final String newValue) {
        final StyleElement element = new StyleElement(name, newValue, "", SelectorSpecificity.DEFAULT_STYLE_ATTRIBUTE);
        localModifications_.put(name, element);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasFeature(final BrowserVersionFeatures property) {
        return getDomElement().hasFeature(property);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BrowserVersion getBrowserVersion() {
        return getDomElement().getPage().getWebClient().getBrowserVersion();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isComputed() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "ComputedCssStyleDeclaration for '" + getDomElement() + "'";
    }

    private String defaultIfEmpty(final String str, final StyleAttributes.Definition definition) {
        return defaultIfEmpty(str, definition, false);
    }

    private String defaultIfEmpty(final String str, final StyleAttributes.Definition definition,
            final boolean isPixel) {
        if (!getDomElement().isAttachedToPage()
                && hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
            return ComputedCssStyleDeclaration.EMPTY_FINAL;
        }
        if (str == null || str.isEmpty()) {
            return definition.getDefaultComputedValue(getBrowserVersion());
        }
        if (isPixel) {
            return pixelString(str);
        }
        return str;
    }

    /**
     * @param toReturnIfEmptyOrDefault the value to return if empty or equals the {@code defaultValue}
     * @param defaultValue the default value of the string
     * @return the string, or {@code toReturnIfEmptyOrDefault}
     */
    private String defaultIfEmpty(final String str, final String toReturnIfEmptyOrDefault, final String defaultValue) {
        if (!getDomElement().isAttachedToPage()
                && hasFeature(CSS_STYLE_PROP_DISCONNECTED_IS_EMPTY)) {
            return ComputedCssStyleDeclaration.EMPTY_FINAL;
        }
        if (str == null || str.isEmpty() || str.equals(defaultValue)) {
            return toReturnIfEmptyOrDefault;
        }
        return str;
    }

    /**
     * Returns the specified length value as a pixel length value, as long as we're not emulating IE.
     * This method does NOT handle percentages correctly; use {@link #pixelValue(Element, CssValue)}
     * if you need percentage support).
     * @param value the length value to convert to a pixel length value
     * @return the specified length value as a pixel length value
     * @see #pixelString(Element, CSSStyleDeclaration.CssValue)
     */
    private static String pixelString(final String value) {
        if (EMPTY_FINAL == value || value.endsWith("px")) {
            return value;
        }
        return CssPixelValueConverter.pixelValue(value) + "px";
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy