/* * Copyright (C) 2011, 2012 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "third_party/blink/renderer/core/css/css_math_expression_node.h" #include "third_party/blink/renderer/core/css/css_numeric_literal_value.h" #include "third_party/blink/renderer/core/css/css_primitive_value_mappings.h" #include "third_party/blink/renderer/core/css/css_value_clamping_utils.h" #include "third_party/blink/renderer/core/css/properties/css_parsing_utils.h" #include "third_party/blink/renderer/core/css/resolver/style_resolver.h" #include "third_party/blink/renderer/platform/geometry/calculation_expression_node.h" #include "third_party/blink/renderer/platform/wtf/math_extras.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" enum ParseState { OK, TooDeep, NoMoreTokens }; namespace blink { static CalculationCategory UnitCategory(CSSPrimitiveValue::UnitType type) { switch (type) { case CSSPrimitiveValue::UnitType::kNumber: case CSSPrimitiveValue::UnitType::kInteger: return kCalcNumber; case CSSPrimitiveValue::UnitType::kPercentage: return kCalcPercent; case CSSPrimitiveValue::UnitType::kEms: case CSSPrimitiveValue::UnitType::kExs: case CSSPrimitiveValue::UnitType::kPixels: case CSSPrimitiveValue::UnitType::kCentimeters: case CSSPrimitiveValue::UnitType::kMillimeters: case CSSPrimitiveValue::UnitType::kQuarterMillimeters: case CSSPrimitiveValue::UnitType::kInches: case CSSPrimitiveValue::UnitType::kPoints: case CSSPrimitiveValue::UnitType::kPicas: case CSSPrimitiveValue::UnitType::kUserUnits: case CSSPrimitiveValue::UnitType::kRems: case CSSPrimitiveValue::UnitType::kChs: case CSSPrimitiveValue::UnitType::kViewportWidth: case CSSPrimitiveValue::UnitType::kViewportHeight: case CSSPrimitiveValue::UnitType::kViewportMin: case CSSPrimitiveValue::UnitType::kViewportMax: return kCalcLength; case CSSPrimitiveValue::UnitType::kContainerWidth: case CSSPrimitiveValue::UnitType::kContainerHeight: case CSSPrimitiveValue::UnitType::kContainerInlineSize: case CSSPrimitiveValue::UnitType::kContainerBlockSize: case CSSPrimitiveValue::UnitType::kContainerMin: case CSSPrimitiveValue::UnitType::kContainerMax: return RuntimeEnabledFeatures::CSSContainerRelativeUnitsEnabled() ? kCalcLength : kCalcOther; case CSSPrimitiveValue::UnitType::kDegrees: case CSSPrimitiveValue::UnitType::kGradians: case CSSPrimitiveValue::UnitType::kRadians: case CSSPrimitiveValue::UnitType::kTurns: return kCalcAngle; case CSSPrimitiveValue::UnitType::kMilliseconds: case CSSPrimitiveValue::UnitType::kSeconds: return kCalcTime; case CSSPrimitiveValue::UnitType::kHertz: case CSSPrimitiveValue::UnitType::kKilohertz: return kCalcFrequency; default: return kCalcOther; } } static bool HasDoubleValue(CSSPrimitiveValue::UnitType type) { switch (type) { case CSSPrimitiveValue::UnitType::kNumber: case CSSPrimitiveValue::UnitType::kPercentage: case CSSPrimitiveValue::UnitType::kEms: case CSSPrimitiveValue::UnitType::kExs: case CSSPrimitiveValue::UnitType::kChs: case CSSPrimitiveValue::UnitType::kRems: case CSSPrimitiveValue::UnitType::kPixels: case CSSPrimitiveValue::UnitType::kCentimeters: case CSSPrimitiveValue::UnitType::kMillimeters: case CSSPrimitiveValue::UnitType::kQuarterMillimeters: case CSSPrimitiveValue::UnitType::kInches: case CSSPrimitiveValue::UnitType::kPoints: case CSSPrimitiveValue::UnitType::kPicas: case CSSPrimitiveValue::UnitType::kUserUnits: case CSSPrimitiveValue::UnitType::kDegrees: case CSSPrimitiveValue::UnitType::kRadians: case CSSPrimitiveValue::UnitType::kGradians: case CSSPrimitiveValue::UnitType::kTurns: case CSSPrimitiveValue::UnitType::kMilliseconds: case CSSPrimitiveValue::UnitType::kSeconds: case CSSPrimitiveValue::UnitType::kHertz: case CSSPrimitiveValue::UnitType::kKilohertz: case CSSPrimitiveValue::UnitType::kViewportWidth: case CSSPrimitiveValue::UnitType::kViewportHeight: case CSSPrimitiveValue::UnitType::kViewportMin: case CSSPrimitiveValue::UnitType::kViewportMax: case CSSPrimitiveValue::UnitType::kContainerWidth: case CSSPrimitiveValue::UnitType::kContainerHeight: case CSSPrimitiveValue::UnitType::kContainerInlineSize: case CSSPrimitiveValue::UnitType::kContainerBlockSize: case CSSPrimitiveValue::UnitType::kContainerMin: case CSSPrimitiveValue::UnitType::kContainerMax: case CSSPrimitiveValue::UnitType::kDotsPerPixel: case CSSPrimitiveValue::UnitType::kDotsPerInch: case CSSPrimitiveValue::UnitType::kDotsPerCentimeter: case CSSPrimitiveValue::UnitType::kFraction: case CSSPrimitiveValue::UnitType::kInteger: return true; default: return false; } } namespace { const PixelsAndPercent CreateClampedSamePixelsAndPercent(float value) { return PixelsAndPercent(CSSValueClampingUtils::ClampLength(value), CSSValueClampingUtils::ClampLength(value)); } bool IsNaN(PixelsAndPercent value, bool allows_negative_percentage_reference) { if (std::isnan(value.pixels + value.percent) || (allows_negative_percentage_reference && std::isinf(value.percent))) { return true; } return false; } absl::optional EvaluateValueIfNaNorInfinity( scoped_refptr value, bool allows_negative_percentage_reference) { float evaluated_value = value->Evaluate(1); if (std::isnan(evaluated_value) || std::isinf(evaluated_value)) { return CreateClampedSamePixelsAndPercent(evaluated_value); } if (allows_negative_percentage_reference) { evaluated_value = value->Evaluate(-1); if (std::isnan(evaluated_value) || std::isinf(evaluated_value)) { return CreateClampedSamePixelsAndPercent(evaluated_value); } } return absl::nullopt; } } // namespace // ------ Start of CSSMathExpressionNumericLiteral member functions ------ // static CSSMathExpressionNumericLiteral* CSSMathExpressionNumericLiteral::Create( const CSSNumericLiteralValue* value, bool is_integer) { return MakeGarbageCollected(value, is_integer); } // static CSSMathExpressionNumericLiteral* CSSMathExpressionNumericLiteral::Create( double value, CSSPrimitiveValue::UnitType type, bool is_integer) { if (!RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled() && (std::isnan(value) || std::isinf(value))) return nullptr; return MakeGarbageCollected( CSSNumericLiteralValue::Create(value, type), is_integer); } CSSMathExpressionNumericLiteral::CSSMathExpressionNumericLiteral( const CSSNumericLiteralValue* value, bool is_integer) : CSSMathExpressionNode(UnitCategory(value->GetType()), is_integer, false /* has_comparisons*/), value_(value) {} bool CSSMathExpressionNumericLiteral::IsZero() const { return !value_->GetDoubleValue(); } String CSSMathExpressionNumericLiteral::CustomCSSText() const { return value_->CssText(); } absl::optional CSSMathExpressionNumericLiteral::ToPixelsAndPercent( const CSSToLengthConversionData& conversion_data) const { PixelsAndPercent value(0, 0); switch (category_) { case kCalcLength: // When CSSCalcInfinityAndNaN is enabled, we allow infinity and NaN in // PixelsAndPercent. Therefore, we need to use a function that doesn't // internally clamp the result to the float range. if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) value.pixels = value_->ComputeLengthPx(conversion_data); else value.pixels = value_->ComputeLength(conversion_data); break; case kCalcPercent: DCHECK(value_->IsPercentage()); // When CSSCalcInfinityAndNaN is enabled, we allow infinity and NaN in // PixelsAndPercent. Therefore, we need to use a function that doesn't // internally clamp the result to the float range. if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) value.percent = value_->GetDoubleValueWithoutClamping(); else value.percent = value_->GetFloatValue(); break; case kCalcNumber: // TODO(alancutter): Stop treating numbers like pixels unconditionally // in calcs to be able to accomodate border-image-width // https://drafts.csswg.org/css-backgrounds-3/#the-border-image-width value.pixels = value_->GetFloatValue() * conversion_data.Zoom(); break; default: NOTREACHED(); } return value; } scoped_refptr CSSMathExpressionNumericLiteral::ToCalculationExpression( const CSSToLengthConversionData& conversion_data) const { return base::MakeRefCounted( *ToPixelsAndPercent(conversion_data)); } double CSSMathExpressionNumericLiteral::DoubleValue() const { if (HasDoubleValue(ResolvedUnitType())) return value_->GetDoubleValueWithoutClamping(); NOTREACHED(); return 0; } absl::optional CSSMathExpressionNumericLiteral::ComputeValueInCanonicalUnit() const { switch (category_) { case kCalcNumber: case kCalcPercent: return value_->DoubleValue(); case kCalcLength: if (CSSPrimitiveValue::IsRelativeUnit(value_->GetType())) return absl::nullopt; U_FALLTHROUGH; case kCalcAngle: case kCalcTime: case kCalcFrequency: return value_->DoubleValue() * CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor( value_->GetType()); default: return absl::nullopt; } } double CSSMathExpressionNumericLiteral::ComputeLengthPx( const CSSToLengthConversionData& conversion_data) const { switch (category_) { case kCalcLength: // When CSSCalcInfinityAndNaN is enabled, we allow infinity and NaN in // PixelsAndPercent. Therefore, we need to use a function that doesn't // internally clamp the result to the float range. if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) return value_->ComputeLengthPx(conversion_data); return value_->ComputeLength(conversion_data); case kCalcNumber: case kCalcPercent: case kCalcAngle: case kCalcFrequency: case kCalcPercentLength: case kCalcTime: case kCalcOther: NOTREACHED(); break; } NOTREACHED(); return 0; } bool CSSMathExpressionNumericLiteral::AccumulateLengthArray( CSSLengthArray& length_array, double multiplier) const { DCHECK_NE(Category(), kCalcNumber); return value_->AccumulateLengthArray(length_array, multiplier); } void CSSMathExpressionNumericLiteral::AccumulateLengthUnitTypes( CSSPrimitiveValue::LengthTypeFlags& types) const { value_->AccumulateLengthUnitTypes(types); } bool CSSMathExpressionNumericLiteral::operator==( const CSSMathExpressionNode& other) const { if (!other.IsNumericLiteral()) return false; return DataEquivalent(value_, To(other).value_); } CSSPrimitiveValue::UnitType CSSMathExpressionNumericLiteral::ResolvedUnitType() const { return value_->GetType(); } bool CSSMathExpressionNumericLiteral::IsComputationallyIndependent() const { return value_->IsComputationallyIndependent(); } void CSSMathExpressionNumericLiteral::Trace(Visitor* visitor) const { visitor->Trace(value_); CSSMathExpressionNode::Trace(visitor); } #if DCHECK_IS_ON() bool CSSMathExpressionNumericLiteral::InvolvesPercentageComparisons() const { return false; } #endif // ------ End of CSSMathExpressionNumericLiteral member functions static const CalculationCategory kAddSubtractResult[kCalcOther][kCalcOther] = { /* CalcNumber */ {kCalcNumber, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther}, /* CalcLength */ {kCalcOther, kCalcLength, kCalcPercentLength, kCalcPercentLength, kCalcOther, kCalcOther, kCalcOther}, /* CalcPercent */ {kCalcOther, kCalcPercentLength, kCalcPercent, kCalcPercentLength, kCalcOther, kCalcOther, kCalcOther}, /* CalcPercentLength */ {kCalcOther, kCalcPercentLength, kCalcPercentLength, kCalcPercentLength, kCalcOther, kCalcOther, kCalcOther}, /* CalcAngle */ {kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcAngle, kCalcOther, kCalcOther}, /* CalcTime */ {kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcTime, kCalcOther}, /* CalcFrequency */ {kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcFrequency}}; static CalculationCategory DetermineCategory( const CSSMathExpressionNode& left_side, const CSSMathExpressionNode& right_side, CSSMathOperator op) { CalculationCategory left_category = left_side.Category(); CalculationCategory right_category = right_side.Category(); if (left_category == kCalcOther || right_category == kCalcOther) return kCalcOther; switch (op) { case CSSMathOperator::kAdd: case CSSMathOperator::kSubtract: return kAddSubtractResult[left_category][right_category]; case CSSMathOperator::kMultiply: if (left_category != kCalcNumber && right_category != kCalcNumber) return kCalcOther; return left_category == kCalcNumber ? right_category : left_category; case CSSMathOperator::kDivide: if (right_category != kCalcNumber || right_side.IsZero()) return kCalcOther; return left_category; default: break; } NOTREACHED(); return kCalcOther; } static bool IsIntegerResult(const CSSMathExpressionNode* left_side, const CSSMathExpressionNode* right_side, CSSMathOperator op) { // Not testing for actual integer values. // Performs W3C spec's type checking for calc integers. // http://www.w3.org/TR/css3-values/#calc-type-checking return op != CSSMathOperator::kDivide && left_side->IsInteger() && right_side->IsInteger(); } // ------ Start of CSSMathExpressionBinaryOperation member functions ------ // static CSSMathExpressionNode* CSSMathExpressionBinaryOperation::Create( const CSSMathExpressionNode* left_side, const CSSMathExpressionNode* right_side, CSSMathOperator op) { DCHECK_NE(left_side->Category(), kCalcOther); DCHECK_NE(right_side->Category(), kCalcOther); CalculationCategory new_category = DetermineCategory(*left_side, *right_side, op); if (new_category == kCalcOther) return nullptr; return MakeGarbageCollected( left_side, right_side, op, new_category); } // static CSSMathExpressionNode* CSSMathExpressionBinaryOperation::CreateSimplified( const CSSMathExpressionNode* left_side, const CSSMathExpressionNode* right_side, CSSMathOperator op) { if (left_side->IsMathFunction() || right_side->IsMathFunction()) return Create(left_side, right_side, op); CalculationCategory left_category = left_side->Category(); CalculationCategory right_category = right_side->Category(); DCHECK_NE(left_category, kCalcOther); DCHECK_NE(right_category, kCalcOther); bool is_integer = IsIntegerResult(left_side, right_side, op); // Simplify numbers. if (left_category == kCalcNumber && right_category == kCalcNumber) { return CSSMathExpressionNumericLiteral::Create( EvaluateOperator(left_side->DoubleValue(), right_side->DoubleValue(), op), CSSPrimitiveValue::UnitType::kNumber, is_integer); } // Simplify addition and subtraction between same types. if (op == CSSMathOperator::kAdd || op == CSSMathOperator::kSubtract) { if (left_category == right_side->Category()) { CSSPrimitiveValue::UnitType left_type = left_side->ResolvedUnitType(); if (HasDoubleValue(left_type)) { CSSPrimitiveValue::UnitType right_type = right_side->ResolvedUnitType(); if (left_type == right_type) { return CSSMathExpressionNumericLiteral::Create( EvaluateOperator(left_side->DoubleValue(), right_side->DoubleValue(), op), left_type, is_integer); } CSSPrimitiveValue::UnitCategory left_unit_category = CSSPrimitiveValue::UnitTypeToUnitCategory(left_type); if (left_unit_category != CSSPrimitiveValue::kUOther && left_unit_category == CSSPrimitiveValue::UnitTypeToUnitCategory(right_type)) { CSSPrimitiveValue::UnitType canonical_type = CSSPrimitiveValue::CanonicalUnitTypeForCategory( left_unit_category); if (canonical_type != CSSPrimitiveValue::UnitType::kUnknown) { double left_value = clampTo( left_side->DoubleValue() * CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor( left_type)); double right_value = clampTo( right_side->DoubleValue() * CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor( right_type)); return CSSMathExpressionNumericLiteral::Create( EvaluateOperator(left_value, right_value, op), canonical_type, is_integer); } } } } } else { // Simplify multiplying or dividing by a number for simplifiable types. DCHECK(op == CSSMathOperator::kMultiply || op == CSSMathOperator::kDivide); const CSSMathExpressionNode* number_side = GetNumberSide(left_side, right_side); if (!number_side) return Create(left_side, right_side, op); if (number_side == left_side && op == CSSMathOperator::kDivide) return nullptr; const CSSMathExpressionNode* other_side = left_side == number_side ? right_side : left_side; double number = number_side->DoubleValue(); if (!RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) { if (std::isnan(number) || std::isinf(number)) return nullptr; if (op == CSSMathOperator::kDivide && !number) return nullptr; } CSSPrimitiveValue::UnitType other_type = other_side->ResolvedUnitType(); if (HasDoubleValue(other_type)) { return CSSMathExpressionNumericLiteral::Create( EvaluateOperator(other_side->DoubleValue(), number, op), other_type, is_integer); } } return Create(left_side, right_side, op); } CSSMathExpressionBinaryOperation::CSSMathExpressionBinaryOperation( const CSSMathExpressionNode* left_side, const CSSMathExpressionNode* right_side, CSSMathOperator op, CalculationCategory category) : CSSMathExpressionNode( category, IsIntegerResult(left_side, right_side, op), left_side->HasComparisons() || right_side->HasComparisons()), left_side_(left_side), right_side_(right_side), operator_(op) {} bool CSSMathExpressionBinaryOperation::IsZero() const { return !DoubleValue(); } absl::optional CSSMathExpressionBinaryOperation::ToPixelsAndPercent( const CSSToLengthConversionData& conversion_data) const { absl::optional result; switch (operator_) { case CSSMathOperator::kAdd: case CSSMathOperator::kSubtract: { result = left_side_->ToPixelsAndPercent(conversion_data); if (!result) return absl::nullopt; absl::optional other_side = right_side_->ToPixelsAndPercent(conversion_data); if (!other_side) return absl::nullopt; if (operator_ == CSSMathOperator::kAdd) { result->pixels += other_side->pixels; result->percent += other_side->percent; } else { result->pixels -= other_side->pixels; result->percent -= other_side->percent; } break; } case CSSMathOperator::kMultiply: case CSSMathOperator::kDivide: { const CSSMathExpressionNode* number_side = GetNumberSide(left_side_, right_side_); const CSSMathExpressionNode* other_side = left_side_ == number_side ? right_side_ : left_side_; result = other_side->ToPixelsAndPercent(conversion_data); if (!result) return absl::nullopt; float number = number_side->DoubleValue(); if (operator_ == CSSMathOperator::kDivide) number = 1.0 / number; result->pixels *= number; result->percent *= number; break; } default: NOTREACHED(); } return result; } scoped_refptr CSSMathExpressionBinaryOperation::ToCalculationExpression( const CSSToLengthConversionData& conversion_data) const { switch (operator_) { case CSSMathOperator::kAdd: return CalculationExpressionAdditiveNode::CreateSimplified( left_side_->ToCalculationExpression(conversion_data), right_side_->ToCalculationExpression(conversion_data), CalculationExpressionAdditiveNode::Type::kAdd); case CSSMathOperator::kSubtract: return CalculationExpressionAdditiveNode::CreateSimplified( left_side_->ToCalculationExpression(conversion_data), right_side_->ToCalculationExpression(conversion_data), CalculationExpressionAdditiveNode::Type::kSubtract); case CSSMathOperator::kMultiply: DCHECK_NE((left_side_->Category() == kCalcNumber), (right_side_->Category() == kCalcNumber)); if (left_side_->Category() == kCalcNumber) { return CalculationExpressionMultiplicationNode::CreateSimplified( right_side_->ToCalculationExpression(conversion_data), left_side_->DoubleValue()); } return CalculationExpressionMultiplicationNode::CreateSimplified( left_side_->ToCalculationExpression(conversion_data), right_side_->DoubleValue()); case CSSMathOperator::kDivide: DCHECK_EQ(right_side_->Category(), kCalcNumber); return CalculationExpressionMultiplicationNode::CreateSimplified( left_side_->ToCalculationExpression(conversion_data), 1.0 / right_side_->DoubleValue()); default: NOTREACHED(); return nullptr; } } double CSSMathExpressionBinaryOperation::DoubleValue() const { DCHECK(HasDoubleValue(ResolvedUnitType())) << CustomCSSText(); return Evaluate(left_side_->DoubleValue(), right_side_->DoubleValue()); } static bool HasCanonicalUnit(CalculationCategory category) { return category == kCalcNumber || category == kCalcLength || category == kCalcPercent || category == kCalcAngle || category == kCalcTime || category == kCalcFrequency; } absl::optional CSSMathExpressionBinaryOperation::ComputeValueInCanonicalUnit() const { if (!HasCanonicalUnit(category_)) return absl::nullopt; absl::optional left_value = left_side_->ComputeValueInCanonicalUnit(); if (!left_value) return absl::nullopt; absl::optional right_value = right_side_->ComputeValueInCanonicalUnit(); if (!right_value) return absl::nullopt; return Evaluate(*left_value, *right_value); } double CSSMathExpressionBinaryOperation::ComputeLengthPx( const CSSToLengthConversionData& conversion_data) const { DCHECK_EQ(kCalcLength, Category()); double left_value; if (left_side_->Category() == kCalcLength) { left_value = left_side_->ComputeLengthPx(conversion_data); } else { DCHECK_EQ(kCalcNumber, left_side_->Category()); left_value = left_side_->DoubleValue(); } double right_value; if (right_side_->Category() == kCalcLength) { right_value = right_side_->ComputeLengthPx(conversion_data); } else { DCHECK_EQ(kCalcNumber, right_side_->Category()); right_value = right_side_->DoubleValue(); } return Evaluate(left_value, right_value); } bool CSSMathExpressionBinaryOperation::AccumulateLengthArray( CSSLengthArray& length_array, double multiplier) const { switch (operator_) { case CSSMathOperator::kAdd: if (!left_side_->AccumulateLengthArray(length_array, multiplier)) return false; if (!right_side_->AccumulateLengthArray(length_array, multiplier)) return false; return true; case CSSMathOperator::kSubtract: if (!left_side_->AccumulateLengthArray(length_array, multiplier)) return false; if (!right_side_->AccumulateLengthArray(length_array, -multiplier)) return false; return true; case CSSMathOperator::kMultiply: DCHECK_NE((left_side_->Category() == kCalcNumber), (right_side_->Category() == kCalcNumber)); if (left_side_->Category() == kCalcNumber) { return right_side_->AccumulateLengthArray( length_array, multiplier * left_side_->DoubleValue()); } else { return left_side_->AccumulateLengthArray( length_array, multiplier * right_side_->DoubleValue()); } case CSSMathOperator::kDivide: DCHECK_EQ(right_side_->Category(), kCalcNumber); return left_side_->AccumulateLengthArray( length_array, multiplier / right_side_->DoubleValue()); default: NOTREACHED(); return false; } } void CSSMathExpressionBinaryOperation::AccumulateLengthUnitTypes( CSSPrimitiveValue::LengthTypeFlags& types) const { left_side_->AccumulateLengthUnitTypes(types); right_side_->AccumulateLengthUnitTypes(types); } bool CSSMathExpressionBinaryOperation::IsComputationallyIndependent() const { if (Category() != kCalcLength && Category() != kCalcPercentLength) return true; return left_side_->IsComputationallyIndependent() && right_side_->IsComputationallyIndependent(); } String CSSMathExpressionBinaryOperation::CustomCSSText() const { StringBuilder result; const bool left_side_needs_parentheses = left_side_->IsBinaryOperation() && operator_ != CSSMathOperator::kAdd; if (left_side_needs_parentheses) result.Append('('); result.Append(left_side_->CustomCSSText()); if (left_side_needs_parentheses) result.Append(')'); result.Append(' '); result.Append(ToString(operator_)); result.Append(' '); const bool right_side_needs_parentheses = right_side_->IsBinaryOperation() && operator_ != CSSMathOperator::kAdd; if (right_side_needs_parentheses) result.Append('('); result.Append(right_side_->CustomCSSText()); if (right_side_needs_parentheses) result.Append(')'); return result.ToString(); } bool CSSMathExpressionBinaryOperation::operator==( const CSSMathExpressionNode& exp) const { if (!exp.IsBinaryOperation()) return false; const CSSMathExpressionBinaryOperation& other = To(exp); return DataEquivalent(left_side_, other.left_side_) && DataEquivalent(right_side_, other.right_side_) && operator_ == other.operator_; } CSSPrimitiveValue::UnitType CSSMathExpressionBinaryOperation::ResolvedUnitType() const { switch (category_) { case kCalcNumber: DCHECK_EQ(left_side_->Category(), kCalcNumber); DCHECK_EQ(right_side_->Category(), kCalcNumber); return CSSPrimitiveValue::UnitType::kNumber; case kCalcLength: case kCalcPercent: { if (left_side_->Category() == kCalcNumber) return right_side_->ResolvedUnitType(); if (right_side_->Category() == kCalcNumber) return left_side_->ResolvedUnitType(); CSSPrimitiveValue::UnitType left_type = left_side_->ResolvedUnitType(); if (left_type == right_side_->ResolvedUnitType()) return left_type; return CSSPrimitiveValue::UnitType::kUnknown; } case kCalcAngle: return CSSPrimitiveValue::UnitType::kDegrees; case kCalcTime: return CSSPrimitiveValue::UnitType::kMilliseconds; case kCalcFrequency: return CSSPrimitiveValue::UnitType::kHertz; case kCalcPercentLength: case kCalcOther: return CSSPrimitiveValue::UnitType::kUnknown; } NOTREACHED(); return CSSPrimitiveValue::UnitType::kUnknown; } void CSSMathExpressionBinaryOperation::Trace(Visitor* visitor) const { visitor->Trace(left_side_); visitor->Trace(right_side_); CSSMathExpressionNode::Trace(visitor); } // static const CSSMathExpressionNode* CSSMathExpressionBinaryOperation::GetNumberSide( const CSSMathExpressionNode* left_side, const CSSMathExpressionNode* right_side) { if (left_side->Category() == kCalcNumber) return left_side; if (right_side->Category() == kCalcNumber) return right_side; return nullptr; } // static double CSSMathExpressionBinaryOperation::EvaluateOperator(double left_value, double right_value, CSSMathOperator op) { // Design doc for infinity and NaN: https://bit.ly/349gXjq switch (op) { case CSSMathOperator::kAdd: if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) return left_value + right_value; return clampTo(left_value + right_value); case CSSMathOperator::kSubtract: if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) return left_value - right_value; return clampTo(left_value - right_value); case CSSMathOperator::kMultiply: if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) return left_value * right_value; return clampTo(left_value * right_value); case CSSMathOperator::kDivide: if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) return left_value / right_value; if (right_value) return clampTo(left_value / right_value); return std::numeric_limits::quiet_NaN(); default: NOTREACHED(); break; } return 0; } #if DCHECK_IS_ON() bool CSSMathExpressionBinaryOperation::InvolvesPercentageComparisons() const { return left_side_->InvolvesPercentageComparisons() || right_side_->InvolvesPercentageComparisons(); } #endif // ------ End of CSSMathExpressionBinaryOperation member functions ------ // ------ Start of CSSMathExpressionVariadicOperation member functions ------ // static CSSMathExpressionVariadicOperation* CSSMathExpressionVariadicOperation::Create( Operands&& operands, CSSMathOperator op) { DCHECK(op == CSSMathOperator::kMin || op == CSSMathOperator::kMax); DCHECK(operands.size()); bool is_first = true; CalculationCategory category; bool is_integer; for (const auto& operand : operands) { if (is_first) { category = operand->Category(); is_integer = operand->IsInteger(); } else { category = kAddSubtractResult[category][operand->Category()]; if (!operand->IsInteger()) is_integer = false; } is_first = false; if (category == kCalcOther) return nullptr; } return MakeGarbageCollected( category, is_integer, std::move(operands), op); } CSSMathExpressionVariadicOperation::CSSMathExpressionVariadicOperation( CalculationCategory category, bool is_integer_result, Operands&& operands, CSSMathOperator op) : CSSMathExpressionNode(category, is_integer_result, true /* has_comparisons */), operands_(std::move(operands)), operator_(op) {} void CSSMathExpressionVariadicOperation::Trace(Visitor* visitor) const { visitor->Trace(operands_); CSSMathExpressionNode::Trace(visitor); } bool CSSMathExpressionVariadicOperation::IsZero() const { absl::optional maybe_value = ComputeValueInCanonicalUnit(); return maybe_value && !*maybe_value; } double CSSMathExpressionVariadicOperation::EvaluateBinary(double lhs, double rhs) const { if (std::isnan(lhs) || std::isnan(rhs)) return std::numeric_limits::quiet_NaN(); switch (operator_) { case CSSMathOperator::kMin: return std::min(lhs, rhs); case CSSMathOperator::kMax: return std::max(lhs, rhs); default: NOTREACHED(); return 0; } } absl::optional CSSMathExpressionVariadicOperation::ComputeValueInCanonicalUnit() const { absl::optional first_value = operands_.front()->ComputeValueInCanonicalUnit(); if (!first_value) return absl::nullopt; double result = *first_value; for (const auto& operand : SecondToLastOperands()) { absl::optional maybe_value = operand->ComputeValueInCanonicalUnit(); if (!maybe_value) return absl::nullopt; result = EvaluateBinary(result, *maybe_value); } return result; } double CSSMathExpressionVariadicOperation::DoubleValue() const { DCHECK(HasDoubleValue(ResolvedUnitType())); double result = operands_.front()->DoubleValue(); for (const auto& operand : SecondToLastOperands()) result = EvaluateBinary(result, operand->DoubleValue()); return result; } double CSSMathExpressionVariadicOperation::ComputeLengthPx( const CSSToLengthConversionData& data) const { DCHECK_EQ(kCalcLength, Category()); double result = operands_.front()->ComputeLengthPx(data); for (const auto& operand : SecondToLastOperands()) result = EvaluateBinary(result, operand->ComputeLengthPx(data)); return result; } String CSSMathExpressionVariadicOperation::CSSTextAsClamp() const { DCHECK(is_clamp_); DCHECK_EQ(CSSMathOperator::kMax, operator_); DCHECK_EQ(2u, operands_.size()); DCHECK(operands_[1]->IsVariadicOperation()); const auto& nested = To(*operands_[1]); DCHECK(!nested.is_clamp_); DCHECK_EQ(CSSMathOperator::kMin, nested.operator_); DCHECK_EQ(2u, nested.operands_.size()); StringBuilder result; result.Append("clamp("); result.Append(operands_[0]->CustomCSSText()); result.Append(", "); result.Append(nested.operands_[0]->CustomCSSText()); result.Append(", "); result.Append(nested.operands_[1]->CustomCSSText()); result.Append(")"); return result.ToString(); } String CSSMathExpressionVariadicOperation::CustomCSSText() const { if (is_clamp_) return CSSTextAsClamp(); StringBuilder result; result.Append(ToString(operator_)); result.Append('('); result.Append(operands_.front()->CustomCSSText()); for (const auto& operand : SecondToLastOperands()) { result.Append(", "); result.Append(operand->CustomCSSText()); } result.Append(')'); return result.ToString(); } absl::optional CSSMathExpressionVariadicOperation::ToPixelsAndPercent( const CSSToLengthConversionData& conversion_data) const { return absl::nullopt; } scoped_refptr CSSMathExpressionVariadicOperation::ToCalculationExpression( const CSSToLengthConversionData& data) const { Vector> operands; operands.ReserveCapacity(operands_.size()); for (const auto& operand : operands_) operands.push_back(operand->ToCalculationExpression(data)); auto expression_type = operator_ == CSSMathOperator::kMin ? CalculationExpressionComparisonNode::Type::kMin : CalculationExpressionComparisonNode::Type::kMax; return CalculationExpressionComparisonNode::CreateSimplified( std::move(operands), expression_type); } bool CSSMathExpressionVariadicOperation::AccumulateLengthArray(CSSLengthArray&, double) const { // When comparison function are involved, we can't resolve the expression into // a length array. // TODO(crbug.com/991672): We need a more general length interpolation // implemetation that doesn't rely on CSSLengthArray. return false; } void CSSMathExpressionVariadicOperation::AccumulateLengthUnitTypes( CSSPrimitiveValue::LengthTypeFlags& types) const { for (const auto& operand : operands_) operand->AccumulateLengthUnitTypes(types); } bool CSSMathExpressionVariadicOperation::IsComputationallyIndependent() const { for (const auto& operand : operands_) { if (!operand->IsComputationallyIndependent()) return false; } return true; } bool CSSMathExpressionVariadicOperation::operator==( const CSSMathExpressionNode& exp) const { if (!exp.IsVariadicOperation()) return false; const CSSMathExpressionVariadicOperation& other = To(exp); if (operator_ != other.operator_) return false; if (operands_.size() != other.operands_.size()) return false; for (wtf_size_t i = 0; i < operands_.size(); ++i) { if (!DataEquivalent(operands_[i], other.operands_[i])) return false; } return true; } CSSPrimitiveValue::UnitType CSSMathExpressionVariadicOperation::ResolvedUnitType() const { if (Category() == kCalcNumber) return CSSPrimitiveValue::UnitType::kNumber; CSSPrimitiveValue::UnitType result = operands_.front()->ResolvedUnitType(); if (result == CSSPrimitiveValue::UnitType::kUnknown) return CSSPrimitiveValue::UnitType::kUnknown; for (const auto& operand : SecondToLastOperands()) { CSSPrimitiveValue::UnitType next = operand->ResolvedUnitType(); if (next == CSSPrimitiveValue::UnitType::kUnknown || next != result) return CSSPrimitiveValue::UnitType::kUnknown; } return result; } #if DCHECK_IS_ON() bool CSSMathExpressionVariadicOperation::InvolvesPercentageComparisons() const { if (Category() == kCalcPercent && operands_.size() > 1u) return true; for (const auto& operand : operands_) { if (operand->InvolvesPercentageComparisons()) return true; } return false; } #endif // ------ End of CSSMathExpressionVariadicOperation member functions class CSSMathExpressionNodeParser { STACK_ALLOCATED(); public: CSSMathExpressionNodeParser() {} CSSMathExpressionNode* ParseCalc(CSSParserTokenRange tokens) { tokens.ConsumeWhitespace(); CSSMathExpressionNode* result = ParseValueExpression(tokens, 0); if (!result || !tokens.AtEnd()) return nullptr; return result; } CSSMathExpressionNode* ParseMinOrMax(CSSParserTokenRange tokens, CSSMathOperator op, int depth) { DCHECK(op == CSSMathOperator::kMin || op == CSSMathOperator::kMax); if (tokens.AtEnd()) return nullptr; CSSMathExpressionVariadicOperation::Operands operands; bool last_token_is_comma = false; while (!tokens.AtEnd()) { tokens.ConsumeWhitespace(); CSSMathExpressionNode* operand = ParseValueExpression(tokens, depth); if (!operand) return nullptr; last_token_is_comma = false; operands.push_back(operand); if (!css_parsing_utils::ConsumeCommaIncludingWhitespace(tokens)) break; last_token_is_comma = true; } if (operands.IsEmpty() || !tokens.AtEnd() || last_token_is_comma) return nullptr; return CSSMathExpressionVariadicOperation::Create(std::move(operands), op); } CSSMathExpressionNode* ParseClamp(CSSParserTokenRange tokens, int depth) { if (tokens.AtEnd()) return nullptr; CSSMathExpressionNode* min_operand = ParseValueExpression(tokens, depth); if (!min_operand) return nullptr; if (!css_parsing_utils::ConsumeCommaIncludingWhitespace(tokens)) return nullptr; CSSMathExpressionNode* val_operand = ParseValueExpression(tokens, depth); if (!val_operand) return nullptr; if (!css_parsing_utils::ConsumeCommaIncludingWhitespace(tokens)) return nullptr; CSSMathExpressionNode* max_operand = ParseValueExpression(tokens, depth); if (!max_operand) return nullptr; if (!tokens.AtEnd()) return nullptr; // clamp(MIN, VAL, MAX) is identical to max(MIN, min(VAL, MAX)) auto* nested = CSSMathExpressionVariadicOperation::Create( {val_operand, max_operand}, CSSMathOperator::kMin); if (!nested) return nullptr; auto* result = CSSMathExpressionVariadicOperation::Create( {min_operand, nested}, CSSMathOperator::kMax); if (!result) return nullptr; result->SetIsClamp(); return result; } private: CSSMathExpressionNode* ParseValue(CSSParserTokenRange& tokens) { CSSParserToken token = tokens.ConsumeIncludingWhitespace(); if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) { if (token.Id() == CSSValueID::kInfinity) { return CSSMathExpressionNumericLiteral::Create( std::numeric_limits::infinity(), CSSPrimitiveValue::UnitType::kNumber, false); } if (token.Id() == CSSValueID::kNegativeInfinity) { return CSSMathExpressionNumericLiteral::Create( -std::numeric_limits::infinity(), CSSPrimitiveValue::UnitType::kNumber, false); } if (token.Id() == CSSValueID::kNan) { return CSSMathExpressionNumericLiteral::Create( std::numeric_limits::quiet_NaN(), CSSPrimitiveValue::UnitType::kNumber, false); } } if (!(token.GetType() == kNumberToken || token.GetType() == kPercentageToken || token.GetType() == kDimensionToken)) return nullptr; CSSPrimitiveValue::UnitType type = token.GetUnitType(); if (UnitCategory(type) == kCalcOther) return nullptr; return CSSMathExpressionNumericLiteral::Create( CSSNumericLiteralValue::Create(token.NumericValue(), type), token.GetNumericValueType() == kIntegerValueType); } CSSMathExpressionNode* ParseValueTerm(CSSParserTokenRange& tokens, int depth) { if (tokens.AtEnd()) return nullptr; if (tokens.Peek().GetType() == kLeftParenthesisToken || tokens.Peek().FunctionId() == CSSValueID::kCalc) { CSSParserTokenRange inner_range = tokens.ConsumeBlock(); tokens.ConsumeWhitespace(); inner_range.ConsumeWhitespace(); CSSMathExpressionNode* result = ParseValueExpression(inner_range, depth); if (!result) return nullptr; result->SetIsNestedCalc(); return result; } if (tokens.Peek().GetType() == kFunctionToken) { CSSValueID function_id = tokens.Peek().FunctionId(); CSSParserTokenRange inner_range = tokens.ConsumeBlock(); tokens.ConsumeWhitespace(); inner_range.ConsumeWhitespace(); switch (function_id) { case CSSValueID::kMin: return ParseMinOrMax(inner_range, CSSMathOperator::kMin, depth); case CSSValueID::kMax: return ParseMinOrMax(inner_range, CSSMathOperator::kMax, depth); case CSSValueID::kClamp: return ParseClamp(inner_range, depth); default: break; } } return ParseValue(tokens); } CSSMathExpressionNode* ParseValueMultiplicativeExpression( CSSParserTokenRange& tokens, int depth) { if (tokens.AtEnd()) return nullptr; CSSMathExpressionNode* result = ParseValueTerm(tokens, depth); if (!result) return nullptr; while (!tokens.AtEnd()) { CSSMathOperator math_operator = ParseCSSArithmeticOperator(tokens.Peek()); if (math_operator != CSSMathOperator::kMultiply && math_operator != CSSMathOperator::kDivide) break; tokens.ConsumeIncludingWhitespace(); CSSMathExpressionNode* rhs = ParseValueTerm(tokens, depth); if (!rhs) return nullptr; result = CSSMathExpressionBinaryOperation::CreateSimplified( result, rhs, math_operator); if (!result) return nullptr; } return result; } CSSMathExpressionNode* ParseAdditiveValueExpression( CSSParserTokenRange& tokens, int depth) { if (tokens.AtEnd()) return nullptr; CSSMathExpressionNode* result = ParseValueMultiplicativeExpression(tokens, depth); if (!result) return nullptr; while (!tokens.AtEnd()) { CSSMathOperator math_operator = ParseCSSArithmeticOperator(tokens.Peek()); if (math_operator != CSSMathOperator::kAdd && math_operator != CSSMathOperator::kSubtract) break; if ((&tokens.Peek() - 1)->GetType() != kWhitespaceToken) return nullptr; // calc(1px+ 2px) is invalid tokens.Consume(); if (tokens.Peek().GetType() != kWhitespaceToken) return nullptr; // calc(1px +2px) is invalid tokens.ConsumeIncludingWhitespace(); CSSMathExpressionNode* rhs = ParseValueMultiplicativeExpression(tokens, depth); if (!rhs) return nullptr; result = CSSMathExpressionBinaryOperation::CreateSimplified( result, rhs, math_operator); if (!result) return nullptr; } return result; } CSSMathExpressionNode* ParseValueExpression(CSSParserTokenRange& tokens, int depth) { if (++depth > kMaxExpressionDepth) return nullptr; return ParseAdditiveValueExpression(tokens, depth); } }; scoped_refptr CSSMathExpressionNode::ToCalcValue( const CSSToLengthConversionData& conversion_data, ValueRange range, bool allows_negative_percentage_reference) const { if (auto maybe_pixels_and_percent = ToPixelsAndPercent(conversion_data)) { // Clamping if pixels + percent could result in NaN. In special case, // inf px + inf % could evaluate to nan when // allows_negative_percentage_reference is true. if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) { if (IsNaN(*maybe_pixels_and_percent, allows_negative_percentage_reference)) { maybe_pixels_and_percent = CreateClampedSamePixelsAndPercent( std::numeric_limits::quiet_NaN()); } else { maybe_pixels_and_percent->pixels = CSSValueClampingUtils::ClampLength( maybe_pixels_and_percent->pixels); maybe_pixels_and_percent->percent = CSSValueClampingUtils::ClampLength( maybe_pixels_and_percent->percent); } } return CalculationValue::Create(*maybe_pixels_and_percent, range); } auto value = ToCalculationExpression(conversion_data); if (RuntimeEnabledFeatures::CSSCalcInfinityAndNaNEnabled()) { absl::optional evaluated_value = EvaluateValueIfNaNorInfinity(value, allows_negative_percentage_reference); if (evaluated_value.has_value()) { return CalculationValue::Create(evaluated_value.value(), range); } } return CalculationValue::CreateSimplified(value, range); } // static CSSMathExpressionNode* CSSMathExpressionNode::Create( const CalculationValue& calc) { if (calc.IsExpression()) return Create(*calc.GetOrCreateExpression()); return Create(calc.GetPixelsAndPercent()); } // static CSSMathExpressionNode* CSSMathExpressionNode::Create(PixelsAndPercent value) { double percent = value.percent; double pixels = value.pixels; CSSMathOperator op = CSSMathOperator::kAdd; if (pixels < 0) { pixels = -pixels; op = CSSMathOperator::kSubtract; } return CSSMathExpressionBinaryOperation::Create( CSSMathExpressionNumericLiteral::Create( CSSNumericLiteralValue::Create( percent, CSSPrimitiveValue::UnitType::kPercentage), percent == trunc(percent)), CSSMathExpressionNumericLiteral::Create( CSSNumericLiteralValue::Create(pixels, CSSPrimitiveValue::UnitType::kPixels), pixels == trunc(pixels)), op); } // static CSSMathExpressionNode* CSSMathExpressionNode::Create( const CalculationExpressionNode& node) { if (node.IsLeaf()) { const auto& leaf = To(node); return Create(leaf.GetPixelsAndPercent()); } if (node.IsMultiplication()) { const auto& multiplication = To(node); double factor = multiplication.GetFactor(); return CSSMathExpressionBinaryOperation::Create( Create(multiplication.GetChild()), CSSMathExpressionNumericLiteral::Create( CSSNumericLiteralValue::Create( factor, CSSPrimitiveValue::UnitType::kNumber), factor == trunc(factor)), CSSMathOperator::kMultiply); } if (node.IsAdditive()) { const auto& add_or_subtract = To(node); auto* lhs = Create(add_or_subtract.GetLeftSide()); auto* rhs = Create(add_or_subtract.GetRightSide()); CSSMathOperator op = add_or_subtract.IsAdd() ? CSSMathOperator::kAdd : CSSMathOperator::kSubtract; return CSSMathExpressionBinaryOperation::Create(lhs, rhs, op); } DCHECK(node.IsComparison()); const auto& comparison = To(node); CSSMathExpressionVariadicOperation::Operands operands; for (const auto& operand : comparison.GetOperands()) operands.push_back(Create(*operand)); CSSMathOperator op = comparison.IsMin() ? CSSMathOperator::kMin : CSSMathOperator::kMax; return CSSMathExpressionVariadicOperation::Create(std::move(operands), op); } // static CSSMathExpressionNode* CSSMathExpressionNode::ParseCalc( const CSSParserTokenRange& tokens) { CSSMathExpressionNodeParser parser; return parser.ParseCalc(tokens); } // static CSSMathExpressionNode* CSSMathExpressionNode::ParseMin( const CSSParserTokenRange& tokens) { CSSMathExpressionNodeParser parser; return parser.ParseMinOrMax(tokens, CSSMathOperator::kMin, 0); } // static CSSMathExpressionNode* CSSMathExpressionNode::ParseMax( const CSSParserTokenRange& tokens) { CSSMathExpressionNodeParser parser; return parser.ParseMinOrMax(tokens, CSSMathOperator::kMax, 0); } // static CSSMathExpressionNode* CSSMathExpressionNode::ParseClamp( const CSSParserTokenRange& tokens) { CSSMathExpressionNodeParser parser; return parser.ParseClamp(tokens, 0); } } // namespace blink