/* * (C) 1999-2003 Lars Knoll (knoll@kde.org) * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All rights reserved. * Copyright (C) 2011 Research In Motion Limited. All rights reserved. * Copyright (C) 2013 Intel Corporation. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "core/css/StylePropertySerializer.h" #include "core/CSSValueKeywords.h" #include "core/StylePropertyShorthand.h" #include "core/css/CSSCustomPropertyDeclaration.h" #include "core/css/CSSPropertyMetadata.h" #include "core/css/CSSValuePool.h" #include "wtf/BitArray.h" #include "wtf/text/StringBuilder.h" namespace blink { static bool isInitialOrInherit(const String& value) { DEFINE_STATIC_LOCAL(String, initial, ("initial")); DEFINE_STATIC_LOCAL(String, inherit, ("inherit")); return value.length() == 7 && (value == initial || value == inherit); } StylePropertySerializer::StylePropertySetForSerializer::StylePropertySetForSerializer(const StylePropertySet& properties) : m_propertySet(&properties) , m_allIndex(m_propertySet->findPropertyIndex(CSSPropertyAll)) , m_needToExpandAll(false) { if (!hasAllProperty()) return; StylePropertySet::PropertyReference allProperty = m_propertySet->propertyAt(m_allIndex); for (unsigned i = 0; i < m_propertySet->propertyCount(); ++i) { StylePropertySet::PropertyReference property = m_propertySet->propertyAt(i); if (CSSProperty::isAffectedByAllProperty(property.id())) { if (allProperty.isImportant() && !property.isImportant()) continue; if (static_cast(m_allIndex) >= i) continue; if (property.value()->equals(*allProperty.value()) && property.isImportant() == allProperty.isImportant()) continue; m_needToExpandAll = true; } if (property.id() < firstCSSProperty || property.id() > lastCSSProperty) continue; m_longhandPropertyUsed.set(property.id() - firstCSSProperty); } } DEFINE_TRACE(StylePropertySerializer::StylePropertySetForSerializer) { visitor->trace(m_propertySet); } unsigned StylePropertySerializer::StylePropertySetForSerializer::propertyCount() const { if (!hasExpandedAllProperty()) return m_propertySet->propertyCount(); return lastCSSProperty - firstCSSProperty + 1; } StylePropertySerializer::PropertyValueForSerializer StylePropertySerializer::StylePropertySetForSerializer::propertyAt(unsigned index) const { if (!hasExpandedAllProperty()) return StylePropertySerializer::PropertyValueForSerializer(m_propertySet->propertyAt(index)); CSSPropertyID propertyID = static_cast(index + firstCSSProperty); ASSERT(firstCSSProperty <= propertyID && propertyID <= lastCSSProperty); if (m_longhandPropertyUsed.get(index)) { int index = m_propertySet->findPropertyIndex(propertyID); ASSERT(index != -1); return StylePropertySerializer::PropertyValueForSerializer(m_propertySet->propertyAt(index)); } StylePropertySet::PropertyReference property = m_propertySet->propertyAt(m_allIndex); return StylePropertySerializer::PropertyValueForSerializer(propertyID, property.value(), property.isImportant()); } bool StylePropertySerializer::StylePropertySetForSerializer::shouldProcessPropertyAt(unsigned index) const { // StylePropertySet has all valid longhands. We should process. if (!hasAllProperty()) return true; // If all is not expanded, we need to process "all" and properties which // are not overwritten by "all". if (!m_needToExpandAll) { StylePropertySet::PropertyReference property = m_propertySet->propertyAt(index); if (property.id() == CSSPropertyAll || !CSSProperty::isAffectedByAllProperty(property.id())) return true; if (property.id() < firstCSSProperty || property.id() > lastCSSProperty) return false; return m_longhandPropertyUsed.get(property.id() - firstCSSProperty); } CSSPropertyID propertyID = static_cast(index + firstCSSProperty); ASSERT(firstCSSProperty <= propertyID && propertyID <= lastCSSProperty); // Since "all" is expanded, we don't need to process "all". // We should not process expanded shorthands (e.g. font, background, // and so on) either. if (isShorthandProperty(propertyID) || propertyID == CSSPropertyAll) return false; // The all property is a shorthand that resets all CSS properties except // direction and unicode-bidi. It only accepts the CSS-wide keywords. // c.f. http://dev.w3.org/csswg/css-cascade/#all-shorthand if (!CSSProperty::isAffectedByAllProperty(propertyID)) return m_longhandPropertyUsed.get(index); return true; } int StylePropertySerializer::StylePropertySetForSerializer::findPropertyIndex(CSSPropertyID propertyID) const { if (!hasExpandedAllProperty()) return m_propertySet->findPropertyIndex(propertyID); return propertyID - firstCSSProperty; } const CSSValue* StylePropertySerializer::StylePropertySetForSerializer::getPropertyCSSValue(CSSPropertyID propertyID) const { int index = findPropertyIndex(propertyID); if (index == -1) return nullptr; StylePropertySerializer::PropertyValueForSerializer value = propertyAt(index); return value.value(); } String StylePropertySerializer::StylePropertySetForSerializer::getPropertyValue(CSSPropertyID propertyID) const { if (!hasExpandedAllProperty()) return m_propertySet->getPropertyValue(propertyID); const CSSValue* value = getPropertyCSSValue(propertyID); if (!value) return String(); return value->cssText(); } bool StylePropertySerializer::StylePropertySetForSerializer::isPropertyImplicit(CSSPropertyID propertyID) const { int index = findPropertyIndex(propertyID); if (index == -1) return false; StylePropertySerializer::PropertyValueForSerializer value = propertyAt(index); return value.isImplicit(); } bool StylePropertySerializer::StylePropertySetForSerializer::propertyIsImportant(CSSPropertyID propertyID) const { int index = findPropertyIndex(propertyID); if (index == -1) return false; StylePropertySerializer::PropertyValueForSerializer value = propertyAt(index); return value.isImportant(); } StylePropertySerializer::StylePropertySerializer(const StylePropertySet& properties) : m_propertySet(properties) { } String StylePropertySerializer::getCustomPropertyText(const PropertyValueForSerializer& property, bool isNotFirstDecl) const { ASSERT(property.id() == CSSPropertyVariable); StringBuilder result; if (isNotFirstDecl) result.append(' '); const CSSCustomPropertyDeclaration* value = toCSSCustomPropertyDeclaration(property.value()); result.append(value->name()); result.append(':'); result.append(value->customCSSText()); if (property.isImportant()) result.appendLiteral(" !important"); result.append(';'); return result.toString(); } static String getApplyAtRuleText(const CSSValue* value, bool isNotFirstDecl) { StringBuilder result; if (isNotFirstDecl) result.append(' '); result.appendLiteral("@apply "); result.append(toCSSCustomIdentValue(value)->value()); result.append(';'); return result.toString(); } String StylePropertySerializer::getPropertyText(CSSPropertyID propertyID, const String& value, bool isImportant, bool isNotFirstDecl) const { StringBuilder result; if (isNotFirstDecl) result.append(' '); result.append(getPropertyName(propertyID)); result.appendLiteral(": "); result.append(value); if (isImportant) result.appendLiteral(" !important"); result.append(';'); return result.toString(); } String StylePropertySerializer::asText() const { StringBuilder result; BitArray shorthandPropertyUsed; BitArray shorthandPropertyAppeared; unsigned size = m_propertySet.propertyCount(); unsigned numDecls = 0; for (unsigned n = 0; n < size; ++n) { if (!m_propertySet.shouldProcessPropertyAt(n)) continue; StylePropertySerializer::PropertyValueForSerializer property = m_propertySet.propertyAt(n); CSSPropertyID propertyID = property.id(); // Only enabled properties should be part of the style. ASSERT(CSSPropertyMetadata::isEnabledProperty(propertyID)); CSSPropertyID shorthandPropertyID = CSSPropertyInvalid; CSSPropertyID borderFallbackShorthandProperty = CSSPropertyInvalid; String value; // Shorthands with variable references are not expanded at parse time // and hence may still be observed during serialization. ASSERT(!isShorthandProperty(propertyID) || property.value()->isVariableReferenceValue()); switch (propertyID) { case CSSPropertyBackgroundAttachment: case CSSPropertyBackgroundClip: case CSSPropertyBackgroundColor: case CSSPropertyBackgroundImage: case CSSPropertyBackgroundOrigin: case CSSPropertyBackgroundPositionX: case CSSPropertyBackgroundPositionY: case CSSPropertyBackgroundSize: case CSSPropertyBackgroundRepeatX: case CSSPropertyBackgroundRepeatY: shorthandPropertyAppeared.set(CSSPropertyBackground - firstCSSProperty); continue; case CSSPropertyBorderTopWidth: case CSSPropertyBorderRightWidth: case CSSPropertyBorderBottomWidth: case CSSPropertyBorderLeftWidth: if (!borderFallbackShorthandProperty) borderFallbackShorthandProperty = CSSPropertyBorderWidth; case CSSPropertyBorderTopStyle: case CSSPropertyBorderRightStyle: case CSSPropertyBorderBottomStyle: case CSSPropertyBorderLeftStyle: if (!borderFallbackShorthandProperty) borderFallbackShorthandProperty = CSSPropertyBorderStyle; case CSSPropertyBorderTopColor: case CSSPropertyBorderRightColor: case CSSPropertyBorderBottomColor: case CSSPropertyBorderLeftColor: if (!borderFallbackShorthandProperty) borderFallbackShorthandProperty = CSSPropertyBorderColor; // FIXME: Deal with cases where only some of border-(top|right|bottom|left) are specified. if (!shorthandPropertyAppeared.get(CSSPropertyBorder - firstCSSProperty)) { value = borderPropertyValue(ReturnNullOnUncommonValues); if (value.isNull()) shorthandPropertyAppeared.set(CSSPropertyBorder - firstCSSProperty); else shorthandPropertyID = CSSPropertyBorder; } else if (shorthandPropertyUsed.get(CSSPropertyBorder - firstCSSProperty)) { shorthandPropertyID = CSSPropertyBorder; } if (!shorthandPropertyID) shorthandPropertyID = borderFallbackShorthandProperty; break; case CSSPropertyBorderTopLeftRadius: case CSSPropertyBorderTopRightRadius: case CSSPropertyBorderBottomLeftRadius: case CSSPropertyBorderBottomRightRadius: shorthandPropertyID = CSSPropertyBorderRadius; break; case CSSPropertyWebkitBorderHorizontalSpacing: case CSSPropertyWebkitBorderVerticalSpacing: shorthandPropertyID = CSSPropertyBorderSpacing; break; case CSSPropertyFontFamily: case CSSPropertyLineHeight: case CSSPropertyFontSize: case CSSPropertyFontStretch: case CSSPropertyFontStyle: case CSSPropertyFontVariant: case CSSPropertyFontWeight: // Don't use CSSPropertyFont because old UAs can't recognize them but are important for editing. break; case CSSPropertyListStyleType: case CSSPropertyListStylePosition: case CSSPropertyListStyleImage: shorthandPropertyID = CSSPropertyListStyle; break; case CSSPropertyMarginTop: case CSSPropertyMarginRight: case CSSPropertyMarginBottom: case CSSPropertyMarginLeft: shorthandPropertyID = CSSPropertyMargin; break; case CSSPropertyMotionPath: case CSSPropertyMotionOffset: case CSSPropertyMotionRotation: shorthandPropertyID = CSSPropertyMotion; break; case CSSPropertyOutlineWidth: case CSSPropertyOutlineStyle: case CSSPropertyOutlineColor: shorthandPropertyID = CSSPropertyOutline; break; case CSSPropertyOverflowX: case CSSPropertyOverflowY: shorthandPropertyID = CSSPropertyOverflow; break; case CSSPropertyPaddingTop: case CSSPropertyPaddingRight: case CSSPropertyPaddingBottom: case CSSPropertyPaddingLeft: shorthandPropertyID = CSSPropertyPadding; break; case CSSPropertyTransitionProperty: case CSSPropertyTransitionDuration: case CSSPropertyTransitionTimingFunction: case CSSPropertyTransitionDelay: shorthandPropertyID = CSSPropertyTransition; break; case CSSPropertyAnimationName: case CSSPropertyAnimationDuration: case CSSPropertyAnimationTimingFunction: case CSSPropertyAnimationDelay: case CSSPropertyAnimationIterationCount: case CSSPropertyAnimationDirection: case CSSPropertyAnimationFillMode: case CSSPropertyAnimationPlayState: shorthandPropertyID = CSSPropertyAnimation; break; case CSSPropertyFlexDirection: case CSSPropertyFlexWrap: shorthandPropertyID = CSSPropertyFlexFlow; break; case CSSPropertyFlexBasis: case CSSPropertyFlexGrow: case CSSPropertyFlexShrink: shorthandPropertyID = CSSPropertyFlex; break; case CSSPropertyWebkitMaskPositionX: case CSSPropertyWebkitMaskPositionY: case CSSPropertyWebkitMaskRepeatX: case CSSPropertyWebkitMaskRepeatY: case CSSPropertyWebkitMaskImage: case CSSPropertyWebkitMaskRepeat: case CSSPropertyWebkitMaskPosition: case CSSPropertyWebkitMaskClip: case CSSPropertyWebkitMaskOrigin: shorthandPropertyID = CSSPropertyWebkitMask; break; case CSSPropertyVariable: result.append(getCustomPropertyText(property, numDecls++)); continue; case CSSPropertyAll: result.append(getPropertyText(propertyID, property.value()->cssText(), property.isImportant(), numDecls++)); continue; case CSSPropertyApplyAtRule: result.append(getApplyAtRuleText(property.value(), numDecls++)); continue; default: break; } unsigned shortPropertyIndex = shorthandPropertyID - firstCSSProperty; if (shorthandPropertyID) { if (shorthandPropertyUsed.get(shortPropertyIndex)) continue; if (!shorthandPropertyAppeared.get(shortPropertyIndex) && value.isNull()) value = m_propertySet.getPropertyValue(shorthandPropertyID); shorthandPropertyAppeared.set(shortPropertyIndex); } if (!value.isNull()) { if (shorthandPropertyID) { propertyID = shorthandPropertyID; shorthandPropertyUsed.set(shortPropertyIndex); } } else { // We should not show "initial" when the "initial" is implicit. // If explicit "initial", we need to show. if (property.value()->isImplicitInitialValue()) continue; value = property.value()->cssText(); } result.append(getPropertyText(propertyID, value, property.isImportant(), numDecls++)); } if (shorthandPropertyAppeared.get(CSSPropertyBackground - firstCSSProperty)) appendBackgroundPropertyAsText(result, numDecls); ASSERT(!numDecls ^ !result.isEmpty()); return result.toString(); } String StylePropertySerializer::getPropertyValue(CSSPropertyID propertyID) const { // Shorthand and 4-values properties switch (propertyID) { case CSSPropertyAnimation: return getLayeredShorthandValue(animationShorthand()); case CSSPropertyBorderSpacing: return borderSpacingValue(borderSpacingShorthand()); case CSSPropertyBackgroundPosition: return getLayeredShorthandValue(backgroundPositionShorthand()); case CSSPropertyBackgroundRepeat: return backgroundRepeatPropertyValue(); case CSSPropertyBackground: return getLayeredShorthandValue(backgroundShorthand()); case CSSPropertyBorder: return borderPropertyValue(OmitUncommonValues); case CSSPropertyBorderTop: return getShorthandValue(borderTopShorthand()); case CSSPropertyBorderRight: return getShorthandValue(borderRightShorthand()); case CSSPropertyBorderBottom: return getShorthandValue(borderBottomShorthand()); case CSSPropertyBorderLeft: return getShorthandValue(borderLeftShorthand()); case CSSPropertyOutline: return getShorthandValue(outlineShorthand()); case CSSPropertyBorderColor: return get4Values(borderColorShorthand()); case CSSPropertyBorderWidth: return get4Values(borderWidthShorthand()); case CSSPropertyBorderStyle: return get4Values(borderStyleShorthand()); case CSSPropertyColumnRule: return getShorthandValue(columnRuleShorthand()); case CSSPropertyColumns: return getShorthandValue(columnsShorthand()); case CSSPropertyFlex: return getShorthandValue(flexShorthand()); case CSSPropertyFlexFlow: return getShorthandValue(flexFlowShorthand()); case CSSPropertyGridColumn: return getShorthandValue(gridColumnShorthand(), " / "); case CSSPropertyGridRow: return getShorthandValue(gridRowShorthand(), " / "); case CSSPropertyGridArea: return getShorthandValue(gridAreaShorthand(), " / "); case CSSPropertyGridGap: return getShorthandValue(gridGapShorthand()); case CSSPropertyFont: return fontValue(); case CSSPropertyMargin: return get4Values(marginShorthand()); case CSSPropertyMotion: return getShorthandValue(motionShorthand()); case CSSPropertyWebkitMarginCollapse: return getShorthandValue(webkitMarginCollapseShorthand()); case CSSPropertyOverflow: return getCommonValue(overflowShorthand()); case CSSPropertyPadding: return get4Values(paddingShorthand()); case CSSPropertyTransition: return getLayeredShorthandValue(transitionShorthand()); case CSSPropertyListStyle: return getShorthandValue(listStyleShorthand()); case CSSPropertyWebkitMaskPosition: return getLayeredShorthandValue(webkitMaskPositionShorthand()); case CSSPropertyWebkitMaskRepeat: return getLayeredShorthandValue(webkitMaskRepeatShorthand()); case CSSPropertyWebkitMask: return getLayeredShorthandValue(webkitMaskShorthand()); case CSSPropertyWebkitTextEmphasis: return getShorthandValue(webkitTextEmphasisShorthand()); case CSSPropertyWebkitTextStroke: return getShorthandValue(webkitTextStrokeShorthand()); case CSSPropertyMarker: { if (const CSSValue* value = m_propertySet.getPropertyCSSValue(CSSPropertyMarkerStart)) return value->cssText(); return String(); } case CSSPropertyBorderRadius: return get4Values(borderRadiusShorthand()); default: return String(); } } String StylePropertySerializer::borderSpacingValue(const StylePropertyShorthand& shorthand) const { const CSSValue* horizontalValue = m_propertySet.getPropertyCSSValue(shorthand.properties()[0]); const CSSValue* verticalValue = m_propertySet.getPropertyCSSValue(shorthand.properties()[1]); // While standard border-spacing property does not allow specifying border-spacing-vertical without // specifying border-spacing-horizontal , // -webkit-border-spacing-vertical can be set without -webkit-border-spacing-horizontal. if (!horizontalValue || !verticalValue) return String(); String horizontalValueCSSText = horizontalValue->cssText(); String verticalValueCSSText = verticalValue->cssText(); if (horizontalValueCSSText == verticalValueCSSText) return horizontalValueCSSText; return horizontalValueCSSText + ' ' + verticalValueCSSText; } void StylePropertySerializer::appendFontLonghandValueIfNotNormal(CSSPropertyID propertyID, StringBuilder& result, String& commonValue) const { int foundPropertyIndex = m_propertySet.findPropertyIndex(propertyID); ASSERT(foundPropertyIndex != -1); const CSSValue* val = m_propertySet.propertyAt(foundPropertyIndex).value(); if (val->isPrimitiveValue() && toCSSPrimitiveValue(val)->getValueID() == CSSValueNormal) { commonValue = String(); return; } char prefix = '\0'; switch (propertyID) { case CSSPropertyFontStyle: break; // No prefix. case CSSPropertyFontFamily: case CSSPropertyFontStretch: case CSSPropertyFontVariant: case CSSPropertyFontWeight: prefix = ' '; break; case CSSPropertyLineHeight: prefix = '/'; break; default: ASSERT_NOT_REACHED(); } if (prefix && !result.isEmpty()) result.append(prefix); String value = m_propertySet.propertyAt(foundPropertyIndex).value()->cssText(); result.append(value); if (!commonValue.isNull() && commonValue != value) commonValue = String(); } String StylePropertySerializer::fontValue() const { if (!isPropertyShorthandAvailable(fontShorthand()) && !shorthandHasOnlyInitialOrInheritedValue(fontShorthand())) return emptyString(); int fontSizePropertyIndex = m_propertySet.findPropertyIndex(CSSPropertyFontSize); int fontFamilyPropertyIndex = m_propertySet.findPropertyIndex(CSSPropertyFontFamily); ASSERT(fontSizePropertyIndex != -1 && fontFamilyPropertyIndex != -1); PropertyValueForSerializer fontSizeProperty = m_propertySet.propertyAt(fontSizePropertyIndex); PropertyValueForSerializer fontFamilyProperty = m_propertySet.propertyAt(fontFamilyPropertyIndex); String commonValue = fontSizeProperty.value()->cssText(); StringBuilder result; appendFontLonghandValueIfNotNormal(CSSPropertyFontStyle, result, commonValue); appendFontLonghandValueIfNotNormal(CSSPropertyFontVariant, result, commonValue); appendFontLonghandValueIfNotNormal(CSSPropertyFontWeight, result, commonValue); appendFontLonghandValueIfNotNormal(CSSPropertyFontStretch, result, commonValue); if (!result.isEmpty()) result.append(' '); result.append(fontSizeProperty.value()->cssText()); appendFontLonghandValueIfNotNormal(CSSPropertyLineHeight, result, commonValue); if (!result.isEmpty()) result.append(' '); result.append(fontFamilyProperty.value()->cssText()); if (isInitialOrInherit(commonValue)) return commonValue; return result.toString(); } String StylePropertySerializer::get4Values(const StylePropertyShorthand& shorthand) const { // Assume the properties are in the usual order top, right, bottom, left. int topValueIndex = m_propertySet.findPropertyIndex(shorthand.properties()[0]); int rightValueIndex = m_propertySet.findPropertyIndex(shorthand.properties()[1]); int bottomValueIndex = m_propertySet.findPropertyIndex(shorthand.properties()[2]); int leftValueIndex = m_propertySet.findPropertyIndex(shorthand.properties()[3]); if (topValueIndex == -1 || rightValueIndex == -1 || bottomValueIndex == -1 || leftValueIndex == -1) return String(); PropertyValueForSerializer top = m_propertySet.propertyAt(topValueIndex); PropertyValueForSerializer right = m_propertySet.propertyAt(rightValueIndex); PropertyValueForSerializer bottom = m_propertySet.propertyAt(bottomValueIndex); PropertyValueForSerializer left = m_propertySet.propertyAt(leftValueIndex); // All 4 properties must be specified. if (!top.value() || !right.value() || !bottom.value() || !left.value()) return String(); if (top.isImportant() != right.isImportant() || right.isImportant() != bottom.isImportant() || bottom.isImportant() != left.isImportant()) return String(); if (top.isInherited() && right.isInherited() && bottom.isInherited() && left.isInherited()) return getValueName(CSSValueInherit); unsigned numInitial = top.value()->isInitialValue() + right.value()->isInitialValue() + bottom.value()->isInitialValue() + left.value()->isInitialValue(); if (numInitial == 4) return getValueName(CSSValueInitial); if (numInitial > 0) return String(); bool showLeft = !right.value()->equals(*left.value()); bool showBottom = !top.value()->equals(*bottom.value()) || showLeft; bool showRight = !top.value()->equals(*right.value()) || showBottom; StringBuilder result; result.append(top.value()->cssText()); if (showRight) { result.append(' '); result.append(right.value()->cssText()); } if (showBottom) { result.append(' '); result.append(bottom.value()->cssText()); } if (showLeft) { result.append(' '); result.append(left.value()->cssText()); } return result.toString(); } String StylePropertySerializer::getLayeredShorthandValue(const StylePropertyShorthand& shorthand) const { const unsigned size = shorthand.length(); // Begin by collecting the properties into a vector. WillBeHeapVector> values(size); // If the below loop succeeds, there should always be at minimum 1 layer. size_t numLayers = 1U; for (size_t i = 0; i < size; i++) { values[i] = m_propertySet.getPropertyCSSValue(shorthand.properties()[i]); // A shorthand is not available if getPropertyCSSValue didn't resolve to anything. if (!values[i]) return String(); if (values[i]->isBaseValueList()) { const CSSValueList* valueList = toCSSValueList(values[i]); numLayers = std::max(numLayers, valueList->length()); } } StringBuilder result; // Tracks whether or not all the values are initial or all the values are inherit. // Start out assuming there is a common value. It will get set to false below if there isn't one. bool hasCommonValue = true; const CSSValue* commonValue = nullptr; // Now stitch the properties together. Implicit initial values are flagged as such and // can safely be omitted. for (size_t layer = 0; layer < numLayers; layer++) { StringBuilder layerResult; bool useRepeatXShorthand = false; bool useRepeatYShorthand = false; bool useSingleWordShorthand = false; bool foundPositionXCSSProperty = false; bool foundPositionYCSSProperty = false; for (unsigned propertyIndex = 0; propertyIndex < size; propertyIndex++) { const CSSValue* value = nullptr; CSSPropertyID property = shorthand.properties()[propertyIndex]; // Get a CSSValue for this property and layer. if (values[propertyIndex]->isBaseValueList()) { // Might return 0 if there is not an item for this layer for this property. value = toCSSValueList(values[propertyIndex])->itemWithBoundsCheck(layer); } else if (layer == 0 || (layer != numLayers - 1 && property == CSSPropertyBackgroundColor)) { // Singletons except background color belong in the 0th layer. // Background color belongs in the last layer. value = values[propertyIndex]; } // No point proceeding if there's not a value to look at. if (!value) continue; // Special case for background-repeat. if ((propertyIndex < size - 1 && m_propertySet.isPropertyImplicit(property)) && (property == CSSPropertyBackgroundRepeatX || property == CSSPropertyWebkitMaskRepeatX)) { ASSERT(shorthand.properties()[propertyIndex + 1] == CSSPropertyBackgroundRepeatY || shorthand.properties()[propertyIndex + 1] == CSSPropertyWebkitMaskRepeatY); const CSSValue* yValue = values[propertyIndex + 1]->isValueList() ? toCSSValueList(values[propertyIndex + 1])->item(layer) : values[propertyIndex + 1].get(); // FIXME: At some point we need to fix this code to avoid returning an invalid shorthand, // since some longhand combinations are not serializable into a single shorthand. if (!value->isPrimitiveValue() || !yValue->isPrimitiveValue()) continue; CSSValueID xId = toCSSPrimitiveValue(value)->getValueID(); CSSValueID yId = toCSSPrimitiveValue(yValue)->getValueID(); // Maybe advance propertyIndex to look at the next CSSValue in the list for the checks below. if (xId == yId) { useSingleWordShorthand = true; property = shorthand.properties()[++propertyIndex]; } else if (xId == CSSValueRepeat && yId == CSSValueNoRepeat) { useRepeatXShorthand = true; property = shorthand.properties()[++propertyIndex]; } else if (xId == CSSValueNoRepeat && yId == CSSValueRepeat) { useRepeatYShorthand = true; property = shorthand.properties()[++propertyIndex]; } } if (!(value->isInitialValue() && toCSSInitialValue(value)->isImplicit())) { if (property == CSSPropertyBackgroundSize || property == CSSPropertyWebkitMaskSize) { if (foundPositionYCSSProperty || foundPositionXCSSProperty) layerResult.appendLiteral(" / "); else layerResult.appendLiteral(" 0% 0% / "); } else if (!layerResult.isEmpty()) { // Do this second to avoid ending up with an extra space in the output if we hit the continue above. layerResult.append(' '); } if (useRepeatXShorthand) { useRepeatXShorthand = false; layerResult.append(getValueName(CSSValueRepeatX)); } else if (useRepeatYShorthand) { useRepeatYShorthand = false; layerResult.append(getValueName(CSSValueRepeatY)); } else { if (useSingleWordShorthand) useSingleWordShorthand = false; layerResult.append(value->cssText()); } if (property == CSSPropertyBackgroundPositionX || property == CSSPropertyWebkitMaskPositionX) foundPositionXCSSProperty = true; if (property == CSSPropertyBackgroundPositionY || property == CSSPropertyWebkitMaskPositionY) { foundPositionYCSSProperty = true; // background-position is a special case. If only the first offset is specified, // the second one defaults to "center", not the same value. if (hasCommonValue && !value->isInitialValue() && !value->isInheritedValue()) hasCommonValue = false; } } if (hasCommonValue && !commonValue) commonValue = value; else if (!value->equals(*commonValue)) hasCommonValue = false; } if (!layerResult.isEmpty()) { if (!result.isEmpty()) result.appendLiteral(", "); result.append(layerResult); } } if (hasCommonValue && (commonValue->isInitialValue() || commonValue->isInheritedValue())) return commonValue->cssText(); return result.toString(); } String StylePropertySerializer::getShorthandValue(const StylePropertyShorthand& shorthand, String separator) const { String commonValue; StringBuilder result; for (unsigned i = 0; i < shorthand.length(); ++i) { const CSSValue* value = m_propertySet.getPropertyCSSValue(shorthand.properties()[i]); if (!value) return String(); String valueText = value->cssText(); if (!i) commonValue = valueText; else if (!commonValue.isNull() && commonValue != valueText) commonValue = String(); if (value->isInitialValue()) continue; if (!result.isEmpty()) result.append(separator); result.append(valueText); } if (isInitialOrInherit(commonValue)) return commonValue; return result.toString(); } // only returns a non-null value if all properties have the same, non-null value String StylePropertySerializer::getCommonValue(const StylePropertyShorthand& shorthand) const { String res; bool lastPropertyWasImportant = false; for (unsigned i = 0; i < shorthand.length(); ++i) { const CSSValue* value = m_propertySet.getPropertyCSSValue(shorthand.properties()[i]); // FIXME: CSSInitialValue::cssText should generate the right value. if (!value) return String(); String text = value->cssText(); if (text.isNull()) return String(); if (res.isNull()) res = text; else if (res != text) return String(); bool currentPropertyIsImportant = m_propertySet.propertyIsImportant(shorthand.properties()[i]); if (i && lastPropertyWasImportant != currentPropertyIsImportant) return String(); lastPropertyWasImportant = currentPropertyIsImportant; } return res; } String StylePropertySerializer::borderPropertyValue(CommonValueMode valueMode) const { const StylePropertyShorthand properties[3] = { borderWidthShorthand(), borderStyleShorthand(), borderColorShorthand() }; String commonValue; StringBuilder result; for (size_t i = 0; i < WTF_ARRAY_LENGTH(properties); ++i) { String value = getCommonValue(properties[i]); if (value.isNull()) { if (valueMode == ReturnNullOnUncommonValues) return String(); ASSERT(valueMode == OmitUncommonValues); continue; } if (!i) commonValue = value; else if (!commonValue.isNull() && commonValue != value) commonValue = String(); if (value == "initial") continue; if (!result.isEmpty()) result.append(' '); result.append(value); } if (isInitialOrInherit(commonValue)) return commonValue; return result.isEmpty() ? String() : result.toString(); } static void appendBackgroundRepeatValue(StringBuilder& builder, const CSSValue& repeatXCSSValue, const CSSValue& repeatYCSSValue) { // FIXME: Ensure initial values do not appear in CSS_VALUE_LISTS. DEFINE_STATIC_REF_WILL_BE_PERSISTENT(CSSPrimitiveValue, initialRepeatValue, (CSSPrimitiveValue::createIdentifier(CSSValueRepeat))); const CSSPrimitiveValue& repeatX = repeatXCSSValue.isInitialValue() ? *initialRepeatValue : toCSSPrimitiveValue(repeatXCSSValue); const CSSPrimitiveValue& repeatY = repeatYCSSValue.isInitialValue() ? *initialRepeatValue : toCSSPrimitiveValue(repeatYCSSValue); CSSValueID repeatXValueId = repeatX.getValueID(); CSSValueID repeatYValueId = repeatY.getValueID(); if (repeatXValueId == repeatYValueId) { builder.append(repeatX.cssText()); } else if (repeatXValueId == CSSValueNoRepeat && repeatYValueId == CSSValueRepeat) { builder.appendLiteral("repeat-y"); } else if (repeatXValueId == CSSValueRepeat && repeatYValueId == CSSValueNoRepeat) { builder.appendLiteral("repeat-x"); } else { builder.append(repeatX.cssText()); builder.appendLiteral(" "); builder.append(repeatY.cssText()); } } String StylePropertySerializer::backgroundRepeatPropertyValue() const { const CSSValue* repeatX = m_propertySet.getPropertyCSSValue(CSSPropertyBackgroundRepeatX); const CSSValue* repeatY = m_propertySet.getPropertyCSSValue(CSSPropertyBackgroundRepeatY); if (!repeatX || !repeatY) return String(); if (m_propertySet.propertyIsImportant(CSSPropertyBackgroundRepeatX) != m_propertySet.propertyIsImportant(CSSPropertyBackgroundRepeatY)) return String(); if ((repeatX->isInitialValue() && repeatY->isInitialValue()) || (repeatX->isInheritedValue() && repeatY->isInheritedValue())) return repeatX->cssText(); const CSSValueList* repeatXList = 0; int repeatXLength = 1; if (repeatX->isValueList()) { repeatXList = toCSSValueList(repeatX); repeatXLength = repeatXList->length(); } else if (!repeatX->isPrimitiveValue()) { return String(); } const CSSValueList* repeatYList = 0; int repeatYLength = 1; if (repeatY->isValueList()) { repeatYList = toCSSValueList(repeatY); repeatYLength = repeatYList->length(); } else if (!repeatY->isPrimitiveValue()) { return String(); } size_t shorthandLength = lowestCommonMultiple(repeatXLength, repeatYLength); StringBuilder builder; for (size_t i = 0; i < shorthandLength; ++i) { if (i) builder.appendLiteral(", "); const CSSValue* xValue = repeatXList ? repeatXList->item(i % repeatXList->length()) : repeatX; const CSSValue* yValue = repeatYList ? repeatYList->item(i % repeatYList->length()) : repeatY; appendBackgroundRepeatValue(builder, *xValue, *yValue); } return builder.toString(); } void StylePropertySerializer::appendBackgroundPropertyAsText(StringBuilder& result, unsigned& numDecls) const { if (isPropertyShorthandAvailable(backgroundShorthand())) { String backgroundValue = getPropertyValue(CSSPropertyBackground); bool isImportant = m_propertySet.propertyIsImportant(CSSPropertyBackgroundImage); result.append(getPropertyText(CSSPropertyBackground, backgroundValue, isImportant, numDecls++)); return; } if (shorthandHasOnlyInitialOrInheritedValue(backgroundShorthand())) { const CSSValue* value = m_propertySet.getPropertyCSSValue(CSSPropertyBackgroundImage); bool isImportant = m_propertySet.propertyIsImportant(CSSPropertyBackgroundImage); result.append(getPropertyText(CSSPropertyBackground, value->cssText(), isImportant, numDecls++)); return; } // backgroundShorthandProperty without layered shorhand properties const CSSPropertyID backgroundPropertyIds[] = { CSSPropertyBackgroundImage, CSSPropertyBackgroundAttachment, CSSPropertyBackgroundColor, CSSPropertyBackgroundSize, CSSPropertyBackgroundOrigin, CSSPropertyBackgroundClip }; for (unsigned i = 0; i < WTF_ARRAY_LENGTH(backgroundPropertyIds); ++i) { CSSPropertyID propertyID = backgroundPropertyIds[i]; const CSSValue* value = m_propertySet.getPropertyCSSValue(propertyID); if (!value) continue; result.append(getPropertyText(propertyID, value->cssText(), m_propertySet.propertyIsImportant(propertyID), numDecls++)); } // FIXME: This is a not-so-nice way to turn x/y positions into single background-position in output. // It is required because background-position-x/y are non-standard properties and WebKit generated output // would not work in Firefox () // It would be a better solution if background-position was UnitType::Pair. if (shorthandHasOnlyInitialOrInheritedValue(backgroundPositionShorthand())) { const CSSValue* value = m_propertySet.getPropertyCSSValue(CSSPropertyBackgroundPositionX); bool isImportant = m_propertySet.propertyIsImportant(CSSPropertyBackgroundPositionX); result.append(getPropertyText(CSSPropertyBackgroundPosition, value->cssText(), isImportant, numDecls++)); } else if (isPropertyShorthandAvailable(backgroundPositionShorthand())) { String positionValue = m_propertySet.getPropertyValue(CSSPropertyBackgroundPosition); bool isImportant = m_propertySet.propertyIsImportant(CSSPropertyBackgroundPositionX); if (!positionValue.isNull()) result.append(getPropertyText(CSSPropertyBackgroundPosition, positionValue, isImportant, numDecls++)); } else { // should check background-position-x or background-position-y. if (const CSSValue* value = m_propertySet.getPropertyCSSValue(CSSPropertyBackgroundPositionX)) { if (!value->isImplicitInitialValue()) { bool isImportant = m_propertySet.propertyIsImportant(CSSPropertyBackgroundPositionX); result.append(getPropertyText(CSSPropertyBackgroundPositionX, value->cssText(), isImportant, numDecls++)); } } if (const CSSValue* value = m_propertySet.getPropertyCSSValue(CSSPropertyBackgroundPositionY)) { if (!value->isImplicitInitialValue()) { bool isImportant = m_propertySet.propertyIsImportant(CSSPropertyBackgroundPositionY); result.append(getPropertyText(CSSPropertyBackgroundPositionY, value->cssText(), isImportant, numDecls++)); } } } String repeatValue = m_propertySet.getPropertyValue(CSSPropertyBackgroundRepeat); if (!repeatValue.isNull()) result.append(getPropertyText(CSSPropertyBackgroundRepeat, repeatValue, m_propertySet.propertyIsImportant(CSSPropertyBackgroundRepeatX), numDecls++)); } bool StylePropertySerializer::isPropertyShorthandAvailable(const StylePropertyShorthand& shorthand) const { ASSERT(shorthand.length() > 0); bool isImportant = m_propertySet.propertyIsImportant(shorthand.properties()[0]); for (unsigned i = 0; i < shorthand.length(); ++i) { const CSSValue* value = m_propertySet.getPropertyCSSValue(shorthand.properties()[i]); if (!value || (value->isInitialValue() && !value->isImplicitInitialValue()) || value->isInheritedValue()) return false; if (isImportant != m_propertySet.propertyIsImportant(shorthand.properties()[i])) return false; } return true; } bool StylePropertySerializer::shorthandHasOnlyInitialOrInheritedValue(const StylePropertyShorthand& shorthand) const { ASSERT(shorthand.length() > 0); bool isImportant = m_propertySet.propertyIsImportant(shorthand.properties()[0]); bool isInitialValue = true; bool isInheritedValue = true; for (unsigned i = 0; i < shorthand.length(); ++i) { const CSSValue* value = m_propertySet.getPropertyCSSValue(shorthand.properties()[i]); if (!value) return false; if (!value->isInitialValue()) isInitialValue = false; if (!value->isInheritedValue()) isInheritedValue = false; if (isImportant != m_propertySet.propertyIsImportant(shorthand.properties()[i])) return false; } return isInitialValue || isInheritedValue; } } // namespace blink