blob: 971966d4928f53f1070d45717b2e10d7eab8466e [file] [log] [blame]
/*
* 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 <algorithm>
#include <cfloat>
#include <numeric>
#include "base/memory/values_equivalent.h"
#include "third_party/blink/renderer/core/css/css_custom_ident_value.h"
#include "third_party/blink/renderer/core/css/css_math_function_value.h"
#include "third_party/blink/renderer/core/css/css_math_operator.h"
#include "third_party/blink/renderer/core/css/css_numeric_literal_value.h"
#include "third_party/blink/renderer/core/css/css_primitive_value.h"
#include "third_party/blink/renderer/core/css/css_value_clamping_utils.h"
#include "third_party/blink/renderer/core/css/parser/css_parser_context.h"
#include "third_party/blink/renderer/core/css/parser/css_parser_token_range.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/core/css/try_tactic_transform.h"
#include "third_party/blink/renderer/core/css_value_keywords.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/style/anchor_specifier_value.h"
#include "third_party/blink/renderer/platform/geometry/calculation_expression_node.h"
#include "third_party/blink/renderer/platform/geometry/length.h"
#include "third_party/blink/renderer/platform/geometry/math_functions.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "ui/gfx/geometry/sin_cos_degrees.h"
namespace blink {
static CalculationResultCategory 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:
case CSSPrimitiveValue::UnitType::kRexs:
case CSSPrimitiveValue::UnitType::kRchs:
case CSSPrimitiveValue::UnitType::kRics:
case CSSPrimitiveValue::UnitType::kRlhs:
case CSSPrimitiveValue::UnitType::kIcs:
case CSSPrimitiveValue::UnitType::kLhs:
return kCalcLength;
case CSSPrimitiveValue::UnitType::kCaps:
case CSSPrimitiveValue::UnitType::kRcaps:
return RuntimeEnabledFeatures::CSSCapFontUnitsEnabled() ? kCalcLength
: kCalcOther;
case CSSPrimitiveValue::UnitType::kViewportInlineSize:
case CSSPrimitiveValue::UnitType::kViewportBlockSize:
case CSSPrimitiveValue::UnitType::kSmallViewportWidth:
case CSSPrimitiveValue::UnitType::kSmallViewportHeight:
case CSSPrimitiveValue::UnitType::kSmallViewportInlineSize:
case CSSPrimitiveValue::UnitType::kSmallViewportBlockSize:
case CSSPrimitiveValue::UnitType::kSmallViewportMin:
case CSSPrimitiveValue::UnitType::kSmallViewportMax:
case CSSPrimitiveValue::UnitType::kLargeViewportWidth:
case CSSPrimitiveValue::UnitType::kLargeViewportHeight:
case CSSPrimitiveValue::UnitType::kLargeViewportInlineSize:
case CSSPrimitiveValue::UnitType::kLargeViewportBlockSize:
case CSSPrimitiveValue::UnitType::kLargeViewportMin:
case CSSPrimitiveValue::UnitType::kLargeViewportMax:
case CSSPrimitiveValue::UnitType::kDynamicViewportWidth:
case CSSPrimitiveValue::UnitType::kDynamicViewportHeight:
case CSSPrimitiveValue::UnitType::kDynamicViewportInlineSize:
case CSSPrimitiveValue::UnitType::kDynamicViewportBlockSize:
case CSSPrimitiveValue::UnitType::kDynamicViewportMin:
case CSSPrimitiveValue::UnitType::kDynamicViewportMax:
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 kCalcLength;
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;
// Resolution units
case CSSPrimitiveValue::UnitType::kDotsPerPixel:
case CSSPrimitiveValue::UnitType::kX:
case CSSPrimitiveValue::UnitType::kDotsPerInch:
case CSSPrimitiveValue::UnitType::kDotsPerCentimeter:
return kCalcResolution;
// Identifier
case CSSPrimitiveValue::UnitType::kIdent:
return kCalcIdent;
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::kIcs:
case CSSPrimitiveValue::UnitType::kLhs:
case CSSPrimitiveValue::UnitType::kCaps:
case CSSPrimitiveValue::UnitType::kRcaps:
case CSSPrimitiveValue::UnitType::kRlhs:
case CSSPrimitiveValue::UnitType::kRems:
case CSSPrimitiveValue::UnitType::kRexs:
case CSSPrimitiveValue::UnitType::kRchs:
case CSSPrimitiveValue::UnitType::kRics:
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::kX:
case CSSPrimitiveValue::UnitType::kDotsPerInch:
case CSSPrimitiveValue::UnitType::kDotsPerCentimeter:
case CSSPrimitiveValue::UnitType::kFlex:
case CSSPrimitiveValue::UnitType::kInteger:
return true;
default:
return false;
}
}
namespace {
double TanDegrees(double degrees) {
// Use table values for tan() if possible.
// We pick a pretty arbitrary limit that should be safe.
if (degrees > -90000000.0 && degrees < 90000000.0) {
// Make sure 0, 45, 90, 135, 180, 225 and 270 degrees get exact results.
double n45degrees = degrees / 45.0;
int octant = static_cast<int>(n45degrees);
if (octant == n45degrees) {
constexpr double kTanN45[] = {
/* 0deg */ 0.0,
/* 45deg */ 1.0,
/* 90deg */ std::numeric_limits<double>::infinity(),
/* 135deg */ -1.0,
/* 180deg */ 0.0,
/* 225deg */ 1.0,
/* 270deg */ -std::numeric_limits<double>::infinity(),
/* 315deg */ -1.0,
};
return kTanN45[octant & 7];
}
}
// Slow path for non-table cases.
double x = Deg2rad(degrees);
return std::tan(x);
}
const PixelsAndPercent CreateClampedSamePixelsAndPercent(float value) {
return PixelsAndPercent(CSSValueClampingUtils::ClampLength(value),
CSSValueClampingUtils::ClampLength(value),
/*has_explicit_pixels=*/true,
/*has_explicit_percent=*/true);
}
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;
}
std::optional<PixelsAndPercent> EvaluateValueIfNaNorInfinity(
scoped_refptr<const blink::CalculationExpressionNode> value,
bool allows_negative_percentage_reference) {
// |input| is not needed because this function is just for handling
// inf and NaN.
float evaluated_value = value->Evaluate(1, {});
if (!std::isfinite(evaluated_value)) {
return CreateClampedSamePixelsAndPercent(evaluated_value);
}
if (allows_negative_percentage_reference) {
evaluated_value = value->Evaluate(-1, {});
if (!std::isfinite(evaluated_value)) {
return CreateClampedSamePixelsAndPercent(evaluated_value);
}
}
return std::nullopt;
}
bool IsAllowedMediaFeature(const CSSValueID& id) {
return id == CSSValueID::kWidth || id == CSSValueID::kHeight;
}
bool CheckProgressFunctionTypes(
CSSValueID function_id,
const CSSMathExpressionOperation::Operands& nodes) {
switch (function_id) {
case CSSValueID::kProgress: {
CalculationResultCategory first_category = nodes[0]->Category();
if (first_category != nodes[1]->Category() ||
first_category != nodes[2]->Category() ||
first_category == CalculationResultCategory::kCalcIntrinsicSize) {
return false;
}
break;
}
// TODO(crbug.com/40944203): For now we only support kCalcLength media
// features
case CSSValueID::kMediaProgress: {
if (!IsAllowedMediaFeature(
To<CSSMathExpressionKeywordLiteral>(*nodes[0]).GetValue())) {
return false;
}
if (nodes[1]->Category() != CalculationResultCategory::kCalcLength ||
nodes[2]->Category() != CalculationResultCategory::kCalcLength) {
return false;
}
break;
}
default:
NOTREACHED();
break;
}
return true;
}
bool CanEagerlySimplify(const CSSMathExpressionNode* operand) {
if (operand->IsOperation()) {
return false;
}
switch (operand->Category()) {
case CalculationResultCategory::kCalcNumber:
case CalculationResultCategory::kCalcAngle:
case CalculationResultCategory::kCalcTime:
case CalculationResultCategory::kCalcFrequency:
case CalculationResultCategory::kCalcResolution:
return true;
case CalculationResultCategory::kCalcLength:
return !CSSPrimitiveValue::IsRelativeUnit(operand->ResolvedUnitType()) &&
!operand->IsAnchorQuery();
default:
return false;
}
}
bool CanEagerlySimplify(const CSSMathExpressionOperation::Operands& operands) {
for (const CSSMathExpressionNode* operand : operands) {
if (!CanEagerlySimplify(operand)) {
return false;
}
}
return true;
}
enum class ProgressArgsSimplificationStatus {
kAllArgsResolveToCanonical,
kAllArgsHaveSameType,
kCanNotSimplify,
};
// Either all the arguments are numerics and have the same unit type (e.g.
// progress(1em from 0em to 1em)), or they are all numerics and can be resolved
// to the canonical unit (e.g. progress(1deg from 0rad to 1deg)). Note: this
// can't be eagerly simplified - progress(1em from 0px to 1em).
ProgressArgsSimplificationStatus CanEagerlySimplifyProgressArgs(
const CSSMathExpressionOperation::Operands& operands) {
if (std::all_of(operands.begin(), operands.end(),
[](const CSSMathExpressionNode* node) {
return node->IsNumericLiteral() &&
node->ComputeValueInCanonicalUnit().has_value();
})) {
return ProgressArgsSimplificationStatus::kAllArgsResolveToCanonical;
}
if (std::all_of(operands.begin(), operands.end(),
[&](const CSSMathExpressionNode* node) {
return node->IsNumericLiteral() &&
node->ResolvedUnitType() ==
operands.front()->ResolvedUnitType();
})) {
return ProgressArgsSimplificationStatus::kAllArgsHaveSameType;
}
return ProgressArgsSimplificationStatus::kCanNotSimplify;
}
using UnitsHashMap = HashMap<CSSPrimitiveValue::UnitType, double>;
struct CSSMathExpressionNodeWithOperator {
DISALLOW_NEW();
public:
CSSMathOperator op;
Member<const CSSMathExpressionNode> node;
CSSMathExpressionNodeWithOperator(CSSMathOperator op,
const CSSMathExpressionNode* node)
: op(op), node(node) {}
void Trace(Visitor* visitor) const { visitor->Trace(node); }
};
using UnitsVector = HeapVector<CSSMathExpressionNodeWithOperator>;
using UnitsVectorHashMap =
HeapHashMap<CSSPrimitiveValue::UnitType, Member<UnitsVector>>;
bool IsNumericNodeWithDoubleValue(const CSSMathExpressionNode* node) {
return node->IsNumericLiteral() && HasDoubleValue(node->ResolvedUnitType());
}
const CSSMathExpressionNode* MaybeNegateFirstNode(
CSSMathOperator op,
const CSSMathExpressionNode* node) {
// If first node's operator is -, negate the value.
if (IsNumericNodeWithDoubleValue(node) && op == CSSMathOperator::kSubtract) {
return CSSMathExpressionNumericLiteral::Create(-node->DoubleValue(),
node->ResolvedUnitType());
}
return node;
}
CSSMathOperator MaybeChangeOperatorSignIfNesting(bool is_in_nesting,
CSSMathOperator outer_op,
CSSMathOperator current_op) {
// For the cases like "a - (b + c)" we need to turn + c into - c.
if (is_in_nesting && outer_op == CSSMathOperator::kSubtract &&
current_op == CSSMathOperator::kAdd) {
return CSSMathOperator::kSubtract;
}
// For the cases like "a - (b - c)" we need to turn - c into + c.
if (is_in_nesting && outer_op == CSSMathOperator::kSubtract &&
current_op == CSSMathOperator::kSubtract) {
return CSSMathOperator::kAdd;
}
// No need to change the sign.
return current_op;
}
CSSMathExpressionNodeWithOperator MaybeReplaceNodeWithCombined(
const CSSMathExpressionNode* node,
CSSMathOperator op,
const UnitsHashMap& units_map) {
if (!node->IsNumericLiteral()) {
return {op, node};
}
CSSPrimitiveValue::UnitType unit_type = node->ResolvedUnitType();
auto it = units_map.find(unit_type);
if (it != units_map.end()) {
double value = it->value;
CSSMathOperator new_op =
value < 0.0f ? CSSMathOperator::kSubtract : CSSMathOperator::kAdd;
CSSMathExpressionNode* new_node =
CSSMathExpressionNumericLiteral::Create(std::abs(value), unit_type);
return {new_op, new_node};
}
return {op, node};
}
// This function combines numeric values that have double value and are of the
// same unit type together in numeric_children and saves all the non add/sub
// operation children and their correct simplified operator in all_children.
void CombineNumericChildrenFromNode(const CSSMathExpressionNode* root,
CSSMathOperator op,
UnitsHashMap& numeric_children,
UnitsVector& all_children,
bool is_in_nesting = false) {
const CSSPrimitiveValue::UnitType unit_type = root->ResolvedUnitType();
// Go deeper inside the operation node if possible.
if (auto* operation = DynamicTo<CSSMathExpressionOperation>(root);
operation && operation->IsAddOrSubtract()) {
const CSSMathOperator operation_op = operation->OperatorType();
is_in_nesting |= operation->IsNestedCalc();
// Nest from the left (first op) to the right (second op).
CombineNumericChildrenFromNode(operation->GetOperands().front(), op,
numeric_children, all_children,
is_in_nesting);
// Change the sign of expression, if we are nesting (inside brackets).
op = MaybeChangeOperatorSignIfNesting(is_in_nesting, op, operation_op);
CombineNumericChildrenFromNode(operation->GetOperands().back(), op,
numeric_children, all_children,
is_in_nesting);
return;
}
// If we have numeric with double value - combine under one unit type.
if (IsNumericNodeWithDoubleValue(root)) {
double value = op == CSSMathOperator::kAdd ? root->DoubleValue()
: -root->DoubleValue();
if (auto it = numeric_children.find(unit_type);
it != numeric_children.end()) {
it->value += value;
} else {
numeric_children.insert(unit_type, value);
}
}
// Save all non add/sub operations.
all_children.emplace_back(op, root);
}
// This function collects numeric values that have double value
// in the numeric_children vector under the same type and saves all the complex
// children and their correct simplified operator in complex_children.
void CollectNumericChildrenFromNode(const CSSMathExpressionNode* root,
CSSMathOperator op,
UnitsVectorHashMap& numeric_children,
UnitsVector& complex_children,
bool is_in_nesting = false) {
// Go deeper inside the operation node if possible.
if (auto* operation = DynamicTo<CSSMathExpressionOperation>(root);
operation && operation->IsAddOrSubtract()) {
const CSSMathOperator operation_op = operation->OperatorType();
is_in_nesting |= operation->IsNestedCalc();
// Nest from the left (first op) to the right (second op).
CollectNumericChildrenFromNode(operation->GetOperands().front(), op,
numeric_children, complex_children,
is_in_nesting);
// Change the sign of expression, if we are nesting (inside brackets).
op = MaybeChangeOperatorSignIfNesting(is_in_nesting, op, operation_op);
CollectNumericChildrenFromNode(operation->GetOperands().back(), op,
numeric_children, complex_children,
is_in_nesting);
return;
}
CSSPrimitiveValue::UnitType unit_type = root->ResolvedUnitType();
// If we have numeric with double value - collect in numeric_children.
if (IsNumericNodeWithDoubleValue(root)) {
if (auto it = numeric_children.find(unit_type);
it != numeric_children.end()) {
it->value->emplace_back(op, root);
} else {
numeric_children.insert(
unit_type, MakeGarbageCollected<UnitsVector>(
1, CSSMathExpressionNodeWithOperator(op, root)));
}
return;
}
// Save all non add/sub operations.
complex_children.emplace_back(op, root);
}
CSSMathExpressionNode* AddNodeToSumNode(CSSMathExpressionNode* sum_node,
const CSSMathExpressionNode* node,
CSSMathOperator op) {
// If the sum node is nullptr, create and return the numeric literal node.
if (!sum_node) {
return MaybeNegateFirstNode(op, node)->Copy();
}
// If the node is numeric with double values,
// add the numeric literal node with |value| and
// operator to match the value's sign.
if (IsNumericNodeWithDoubleValue(node)) {
double value = node->DoubleValue();
CSSMathExpressionNode* new_node = CSSMathExpressionNumericLiteral::Create(
std::abs(value), node->ResolvedUnitType());
// Change the operator correctly.
if (value < 0.0f && op == CSSMathOperator::kAdd) {
// + -10 -> -10
op = CSSMathOperator::kSubtract;
} else if (value < 0.0f && op == CSSMathOperator::kSubtract) {
// - -10 -> + 10.
op = CSSMathOperator::kAdd;
}
return MakeGarbageCollected<CSSMathExpressionOperation>(
sum_node, new_node, op, sum_node->Category());
}
// Add the node to the sum_node otherwise.
return MakeGarbageCollected<CSSMathExpressionOperation>(sum_node, node, op,
sum_node->Category());
}
CSSMathExpressionNode* AddNodesVectorToSumNode(CSSMathExpressionNode* sum_node,
const UnitsVector& vector) {
for (const auto& [op, node] : vector) {
sum_node = AddNodeToSumNode(sum_node, node, op);
}
return sum_node;
}
// This function follows:
// https://drafts.csswg.org/css-values-4/#sort-a-calculations-children
// As in Blink the math expression tree is binary, we need to collect all the
// elements of this tree together and create a new tree as a result.
CSSMathExpressionNode* MaybeSortSumNode(
const CSSMathExpressionOperation* root) {
CHECK(root->IsAddOrSubtract());
CHECK_EQ(root->GetOperands().size(), 2u);
// Hash map of vectors of numeric literal values with double value with the
// same unit type.
UnitsVectorHashMap numeric_children;
// Vector of all non add/sub operation children.
UnitsVector complex_children;
// Collect all the numeric literal with double value in one vector.
// Note: using kAdd here as the operator for the first child
// (e.g. a - b = +a - b, a + b = +a + b)
CollectNumericChildrenFromNode(root, CSSMathOperator::kAdd, numeric_children,
complex_children, false);
// Form the final node.
CSSMathExpressionNode* final_node = nullptr;
// From spec: If nodes contains a number, remove it from nodes and append it
// to ret.
if (auto it = numeric_children.find(CSSPrimitiveValue::UnitType::kNumber);
it != numeric_children.end()) {
final_node = AddNodesVectorToSumNode(final_node, *it->value);
numeric_children.erase(it);
}
// From spec: If nodes contains a percentage, remove it from nodes and append
// it to ret.
if (auto it = numeric_children.find(CSSPrimitiveValue::UnitType::kPercentage);
it != numeric_children.end()) {
final_node = AddNodesVectorToSumNode(final_node, *it->value);
numeric_children.erase(it);
}
// Now, sort the rest numeric values alphabatically.
// From spec: If nodes contains any dimensions, remove them from nodes, sort
// them by their units, ordered ASCII case-insensitively, and append them to
// ret.
auto comp = [&](const CSSPrimitiveValue::UnitType& key_a,
const CSSPrimitiveValue::UnitType& key_b) {
return strcmp(CSSPrimitiveValue::UnitTypeToString(key_a),
CSSPrimitiveValue::UnitTypeToString(key_b)) < 0;
};
Vector<CSSPrimitiveValue::UnitType> keys;
keys.reserve(numeric_children.size());
for (const CSSPrimitiveValue::UnitType& key : numeric_children.Keys()) {
keys.push_back(key);
}
std::sort(keys.begin(), keys.end(), comp);
// Now, add those numeric nodes in the sorted order.
for (const auto& unit_type : keys) {
final_node =
AddNodesVectorToSumNode(final_node, *numeric_children.at(unit_type));
}
// Now, add all the complex (non-numerics with double value) values.
final_node = AddNodesVectorToSumNode(final_node, complex_children);
return final_node;
}
// This function follows:
// https://drafts.csswg.org/css-values-4/#calc-simplification
// As in Blink the math expression tree is binary, we need to collect all the
// elements of this tree together and create a new tree as a result.
CSSMathExpressionNode* MaybeSimplifySumNode(
const CSSMathExpressionOperation* root) {
CHECK(root->IsAddOrSubtract());
CHECK_EQ(root->GetOperands().size(), 2u);
// Hash map of numeric literal values of the same type, that can be
// combined together.
UnitsHashMap numeric_children;
// Vector of all non add/sub operation children.
UnitsVector all_children;
// Collect all the numeric literal values together.
// Note: using kAdd here as the operator for the first child
// (e.g. a - b = +a - b, a + b = +a + b)
CombineNumericChildrenFromNode(root, CSSMathOperator::kAdd, numeric_children,
all_children);
// Form the final node.
HashSet<CSSPrimitiveValue::UnitType> used_units;
CSSMathExpressionNode* final_node = nullptr;
for (const auto& child : all_children) {
auto [op, node] =
MaybeReplaceNodeWithCombined(child.node, child.op, numeric_children);
CSSPrimitiveValue::UnitType unit_type = node->ResolvedUnitType();
// Skip already used unit types, as they have been already combined.
if (IsNumericNodeWithDoubleValue(node)) {
if (used_units.Contains(unit_type)) {
continue;
}
used_units.insert(unit_type);
}
if (!final_node) {
// First child.
final_node = MaybeNegateFirstNode(op, node)->Copy();
continue;
}
final_node = MakeGarbageCollected<CSSMathExpressionOperation>(
final_node, node, op, root->Category());
}
return final_node;
}
CSSMathExpressionNode* MaybeDistributeArithmeticOperation(
const CSSMathExpressionNode* left_side,
const CSSMathExpressionNode* right_side,
CSSMathOperator op) {
if (op != CSSMathOperator::kMultiply && op != CSSMathOperator::kDivide) {
return nullptr;
}
// NOTE: we should not simplify num * (fn + fn), all the operands inside
// the sum should be numeric.
// Case (Op1 + Op2) * Num.
auto* left_operation = DynamicTo<CSSMathExpressionOperation>(left_side);
auto* right_numeric = DynamicTo<CSSMathExpressionNumericLiteral>(right_side);
if (left_operation && left_operation->IsAddOrSubtract() &&
left_operation->AllOperandsAreNumeric() && right_numeric &&
right_numeric->Category() == CalculationResultCategory::kCalcNumber) {
auto* new_left_side =
CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
left_operation->GetOperands().front(), right_side, op);
auto* new_right_side =
CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
left_operation->GetOperands().back(), right_side, op);
CSSMathExpressionNode* operation =
CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
new_left_side, new_right_side, left_operation->OperatorType());
// Note: setting SetIsNestedCalc is needed, as we can be in this situation:
// A - B * (C + D)
// /\/\/\/\/\ - we are B * (C + D)
// and we don't know about the -, as it's another operation,
// so make the simplified operation nested to end up with:
// A - (B * C + B * D).
operation->SetIsNestedCalc();
return operation;
}
// Case Num * (Op1 + Op2). But don't do num / (Op1 + Op2), as it can invert
// the type.
auto* right_operation = DynamicTo<CSSMathExpressionOperation>(right_side);
auto* left_numeric = DynamicTo<CSSMathExpressionNumericLiteral>(left_side);
if (right_operation && right_operation->IsAddOrSubtract() &&
right_operation->AllOperandsAreNumeric() && left_numeric &&
left_numeric->Category() == CalculationResultCategory::kCalcNumber &&
op != CSSMathOperator::kDivide) {
auto* new_right_side =
CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
left_side, right_operation->GetOperands().front(), op);
auto* new_left_side =
CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
left_side, right_operation->GetOperands().back(), op);
CSSMathExpressionNode* operation =
CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
new_right_side, new_left_side, right_operation->OperatorType());
// Note: setting SetIsNestedCalc is needed, as we can be in this situation:
// A - (C + D) * B
// /\/\/\/\/\ - we are (C + D) * B
// and we don't know about the -, as it's another operation,
// so make the simplified operation nested to end up with:
// A - (B * C + B * D).
operation->SetIsNestedCalc();
return operation;
}
return nullptr;
}
} // namespace
// ------ Start of CSSMathExpressionNumericLiteral member functions ------
// static
CSSMathExpressionNumericLiteral* CSSMathExpressionNumericLiteral::Create(
const CSSNumericLiteralValue* value) {
return MakeGarbageCollected<CSSMathExpressionNumericLiteral>(value);
}
// static
CSSMathExpressionNumericLiteral* CSSMathExpressionNumericLiteral::Create(
double value,
CSSPrimitiveValue::UnitType type) {
return MakeGarbageCollected<CSSMathExpressionNumericLiteral>(
CSSNumericLiteralValue::Create(value, type));
}
CSSMathExpressionNumericLiteral::CSSMathExpressionNumericLiteral(
const CSSNumericLiteralValue* value)
: CSSMathExpressionNode(
UnitCategory(value->GetType()),
false /* has_comparisons*/,
false /* needs_tree_scope_population*/),
value_(value) {
if (!value_->IsNumber() && CanEagerlySimplify(this)) {
// "If root is a dimension that is not expressed in its canonical unit, and
// there is enough information available to convert it to the canonical
// unit, do so, and return the value."
// https://w3c.github.io/csswg-drafts/css-values/#calc-simplification
//
// However, Numbers should not be eagerly simplified here since that would
// result in converting Integers to Doubles (kNumber, canonical unit for
// Numbers).
value_ = value_->CreateCanonicalUnitValue();
}
}
bool CSSMathExpressionNumericLiteral::IsZero() const {
return !value_->GetDoubleValue();
}
String CSSMathExpressionNumericLiteral::CustomCSSText() const {
return value_->CssText();
}
std::optional<PixelsAndPercent>
CSSMathExpressionNumericLiteral::ToPixelsAndPercent(
const CSSLengthResolver& length_resolver) const {
switch (category_) {
case kCalcLength:
return PixelsAndPercent(value_->ComputeLengthPx(length_resolver), 0.0f,
/*has_explicit_pixels=*/true,
/*has_explicit_percent=*/false);
case kCalcPercent:
DCHECK(value_->IsPercentage());
return PixelsAndPercent(0.0f, value_->GetDoubleValueWithoutClamping(),
/*has_explicit_pixels=*/false,
/*has_explicit_percent=*/true);
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
return PixelsAndPercent(value_->GetFloatValue() * length_resolver.Zoom(),
0.0f, /*has_explicit_pixels=*/true,
/*has_explicit_percent=*/false);
default:
NOTREACHED();
return {};
}
}
scoped_refptr<const CalculationExpressionNode>
CSSMathExpressionNumericLiteral::ToCalculationExpression(
const CSSLengthResolver& length_resolver) const {
if (Category() == kCalcNumber) {
return base::MakeRefCounted<CalculationExpressionNumberNode>(
value_->DoubleValue());
}
return base::MakeRefCounted<CalculationExpressionPixelsAndPercentNode>(
*ToPixelsAndPercent(length_resolver));
}
double CSSMathExpressionNumericLiteral::DoubleValue() const {
if (HasDoubleValue(ResolvedUnitType())) {
return value_->GetDoubleValueWithoutClamping();
}
NOTREACHED();
return 0;
}
std::optional<double>
CSSMathExpressionNumericLiteral::ComputeValueInCanonicalUnit() const {
switch (category_) {
case kCalcNumber:
case kCalcPercent:
return value_->DoubleValue();
case kCalcLength:
if (CSSPrimitiveValue::IsRelativeUnit(value_->GetType())) {
return std::nullopt;
}
[[fallthrough]];
case kCalcAngle:
case kCalcTime:
case kCalcFrequency:
case kCalcResolution:
return value_->DoubleValue() *
CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor(
value_->GetType());
default:
return std::nullopt;
}
}
double CSSMathExpressionNumericLiteral::ComputeDouble(
const CSSLengthResolver& length_resolver) const {
switch (category_) {
case kCalcLength:
return value_->ComputeLengthPx(length_resolver);
case kCalcPercent:
case kCalcNumber:
return value_->DoubleValue();
case kCalcAngle:
return value_->ComputeDegrees();
case kCalcTime:
return value_->ComputeSeconds();
case kCalcResolution:
return value_->ComputeDotsPerPixel();
case kCalcFrequency:
return value_->ComputeInCanonicalUnit();
case kCalcLengthFunction:
case kCalcIntrinsicSize:
case kCalcOther:
case kCalcIdent:
NOTREACHED();
break;
}
NOTREACHED();
return 0;
}
double CSSMathExpressionNumericLiteral::ComputeLengthPx(
const CSSLengthResolver& length_resolver) const {
switch (category_) {
case kCalcLength:
return value_->ComputeLengthPx(length_resolver);
case kCalcNumber:
case kCalcPercent:
case kCalcAngle:
case kCalcFrequency:
case kCalcLengthFunction:
case kCalcIntrinsicSize:
case kCalcTime:
case kCalcResolution:
case kCalcOther:
case kCalcIdent:
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 base::ValuesEquivalent(
value_, To<CSSMathExpressionNumericLiteral>(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 CalculationResultCategory
kAddSubtractResult[kCalcOther][kCalcOther] = {
/* CalcNumber */
{kCalcNumber, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther},
/* CalcLength */
{kCalcOther, kCalcLength, kCalcLengthFunction, kCalcLengthFunction,
kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther},
/* CalcPercent */
{kCalcOther, kCalcLengthFunction, kCalcPercent, kCalcLengthFunction,
kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther},
/* CalcLengthFunction */
{kCalcOther, kCalcLengthFunction, kCalcLengthFunction,
kCalcLengthFunction, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther, kCalcOther},
/* CalcIntrinsicSize */
{kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther, kCalcOther, kCalcOther, kCalcOther},
/* CalcAngle */
{kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcAngle,
kCalcOther, kCalcOther, kCalcOther, kCalcOther},
/* CalcTime */
{kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcTime, kCalcOther, kCalcOther, kCalcOther},
/* CalcFrequency */
{kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther, kCalcFrequency, kCalcOther, kCalcOther},
/* CalcResolution */
{kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther, kCalcOther, kCalcResolution, kCalcOther},
/* CalcIdent */
{kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther, kCalcOther,
kCalcOther, kCalcOther, kCalcOther, kCalcOther},
};
static CalculationResultCategory DetermineCategory(
const CSSMathExpressionNode& left_side,
const CSSMathExpressionNode& right_side,
CSSMathOperator op) {
CalculationResultCategory left_category = left_side.Category();
CalculationResultCategory right_category = right_side.Category();
if (left_category == kCalcOther || right_category == kCalcOther) {
return kCalcOther;
}
if (left_category == kCalcIntrinsicSize ||
right_category == kCalcIntrinsicSize) {
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) {
return kCalcOther;
}
return left_category;
default:
break;
}
NOTREACHED();
return kCalcOther;
}
static CalculationResultCategory DetermineComparisonCategory(
const CSSMathExpressionOperation::Operands& operands) {
DCHECK(!operands.empty());
bool is_first = true;
CalculationResultCategory category = kCalcOther;
for (const CSSMathExpressionNode* operand : operands) {
if (is_first) {
category = operand->Category();
} else {
category = kAddSubtractResult[category][operand->Category()];
}
is_first = false;
if (category == kCalcOther) {
break;
}
}
return category;
}
static CalculationResultCategory DetermineCalcSizeCategory(
const CSSMathExpressionNode& left_side,
const CSSMathExpressionNode& right_side,
CSSMathOperator op) {
CalculationResultCategory basis_category = left_side.Category();
CalculationResultCategory calculation_category = right_side.Category();
if ((basis_category == kCalcLength || basis_category == kCalcPercent ||
basis_category == kCalcLengthFunction ||
basis_category == kCalcIntrinsicSize) &&
(calculation_category == kCalcLength ||
calculation_category == kCalcPercent ||
calculation_category == kCalcLengthFunction)) {
return kCalcIntrinsicSize;
}
return kCalcOther;
}
// ------ Start of CSSMathExpressionIdentifierLiteral member functions -
CSSMathExpressionIdentifierLiteral::CSSMathExpressionIdentifierLiteral(
AtomicString identifier)
: CSSMathExpressionNode(UnitCategory(CSSPrimitiveValue::UnitType::kIdent),
false /* has_comparisons*/,
false /* needs_tree_scope_population*/),
identifier_(std::move(identifier)) {}
scoped_refptr<const CalculationExpressionNode>
CSSMathExpressionIdentifierLiteral::ToCalculationExpression(
const CSSLengthResolver&) const {
return base::MakeRefCounted<CalculationExpressionIdentifierNode>(identifier_);
}
// ------ End of CSSMathExpressionIdentifierLiteral member functions ----
// ------ Start of CSSMathExpressionKeywordLiteral member functions -
namespace {
CalculationExpressionSizingKeywordNode::Keyword CSSValueIDToSizingKeyword(
CSSValueID keyword) {
// The keywords supported here should be the ones supported in
// css_parsing_utils::ValidWidthOrHeightKeyword plus 'any', 'auto' and 'size'.
// This should also match SizingKeywordToCSSValueID below.
switch (keyword) {
#define KEYWORD_CASE(kw) \
case CSSValueID::kw: \
return CalculationExpressionSizingKeywordNode::Keyword::kw;
KEYWORD_CASE(kAny)
KEYWORD_CASE(kSize)
KEYWORD_CASE(kAuto)
KEYWORD_CASE(kMinContent)
KEYWORD_CASE(kWebkitMinContent)
KEYWORD_CASE(kMaxContent)
KEYWORD_CASE(kWebkitMaxContent)
KEYWORD_CASE(kFitContent)
KEYWORD_CASE(kWebkitFitContent)
KEYWORD_CASE(kWebkitFillAvailable)
#undef KEYWORD_CASE
default:
break;
}
NOTREACHED_NORETURN();
}
CSSValueID SizingKeywordToCSSValueID(
CalculationExpressionSizingKeywordNode::Keyword keyword) {
// This should match CSSValueIDToSizingKeyword above.
switch (keyword) {
#define KEYWORD_CASE(kw) \
case CalculationExpressionSizingKeywordNode::Keyword::kw: \
return CSSValueID::kw;
KEYWORD_CASE(kAny)
KEYWORD_CASE(kSize)
KEYWORD_CASE(kAuto)
KEYWORD_CASE(kMinContent)
KEYWORD_CASE(kWebkitMinContent)
KEYWORD_CASE(kMaxContent)
KEYWORD_CASE(kWebkitMaxContent)
KEYWORD_CASE(kFitContent)
KEYWORD_CASE(kWebkitFitContent)
KEYWORD_CASE(kWebkitFillAvailable)
#undef KEYWORD_CASE
}
NOTREACHED_NORETURN();
}
CalculationResultCategory DetermineKeywordCategory(CSSValueID keyword,
CSSMathOperator op) {
switch (op) {
case CSSMathOperator::kMediaProgress:
return kCalcLength;
case CSSMathOperator::kCalcSize:
return kCalcLengthFunction;
default:
NOTREACHED_NORETURN();
};
}
} // namespace
CSSMathExpressionKeywordLiteral::CSSMathExpressionKeywordLiteral(
CSSValueID keyword,
CSSMathOperator op)
: CSSMathExpressionNode(DetermineKeywordCategory(keyword, op),
false /* has_comparisons*/,
false /* needs_tree_scope_population*/),
keyword_(keyword),
operator_(op) {}
scoped_refptr<const CalculationExpressionNode>
CSSMathExpressionKeywordLiteral::ToCalculationExpression(
const CSSLengthResolver& length_resolver) const {
switch (operator_) {
case CSSMathOperator::kMediaProgress: {
switch (keyword_) {
case CSSValueID::kWidth:
return base::MakeRefCounted<
CalculationExpressionPixelsAndPercentNode>(
PixelsAndPercent(length_resolver.ViewportWidth()));
case CSSValueID::kHeight:
return base::MakeRefCounted<
CalculationExpressionPixelsAndPercentNode>(
PixelsAndPercent(length_resolver.ViewportHeight()));
default:
NOTREACHED_NORETURN();
}
}
case CSSMathOperator::kCalcSize:
return base::MakeRefCounted<CalculationExpressionSizingKeywordNode>(
CSSValueIDToSizingKeyword(keyword_));
default:
NOTREACHED_NORETURN();
};
}
double CSSMathExpressionKeywordLiteral::ComputeDouble(
const CSSLengthResolver& length_resolver) const {
switch (operator_) {
case CSSMathOperator::kMediaProgress: {
switch (keyword_) {
case CSSValueID::kWidth:
return length_resolver.ViewportWidth();
case CSSValueID::kHeight:
return length_resolver.ViewportHeight();
default:
NOTREACHED_NORETURN();
}
}
case CSSMathOperator::kCalcSize:
NOTREACHED_NORETURN();
default:
NOTREACHED_NORETURN();
};
}
std::optional<PixelsAndPercent>
CSSMathExpressionKeywordLiteral::ToPixelsAndPercent(
const CSSLengthResolver& length_resolver) const {
switch (operator_) {
case CSSMathOperator::kMediaProgress:
switch (keyword_) {
case CSSValueID::kWidth:
return PixelsAndPercent(length_resolver.ViewportWidth());
case CSSValueID::kHeight:
return PixelsAndPercent(length_resolver.ViewportHeight());
default:
NOTREACHED_NORETURN();
}
default:
return std::nullopt;
}
}
// ------ End of CSSMathExpressionKeywordLiteral member functions ----
// ------ Start of CSSMathExpressionOperation member functions ------
bool CSSMathExpressionOperation::AllOperandsAreNumeric() const {
return std::all_of(
operands_.begin(), operands_.end(),
[](const CSSMathExpressionNode* op) { return op->IsNumericLiteral(); });
}
// static
CSSMathExpressionNode* CSSMathExpressionOperation::CreateArithmeticOperation(
const CSSMathExpressionNode* left_side,
const CSSMathExpressionNode* right_side,
CSSMathOperator op) {
DCHECK_NE(left_side->Category(), kCalcOther);
DCHECK_NE(right_side->Category(), kCalcOther);
CalculationResultCategory new_category =
DetermineCategory(*left_side, *right_side, op);
if (new_category == kCalcOther) {
return nullptr;
}
return MakeGarbageCollected<CSSMathExpressionOperation>(left_side, right_side,
op, new_category);
}
// static
CSSMathExpressionNode* CSSMathExpressionOperation::CreateCalcSizeOperation(
const CSSMathExpressionNode* left_side,
const CSSMathExpressionNode* right_side) {
DCHECK_NE(left_side->Category(), kCalcOther);
DCHECK_NE(right_side->Category(), kCalcOther);
const CSSMathOperator op = CSSMathOperator::kCalcSize;
CalculationResultCategory new_category =
DetermineCalcSizeCategory(*left_side, *right_side, op);
if (new_category == kCalcOther) {
return nullptr;
}
return MakeGarbageCollected<CSSMathExpressionOperation>(left_side, right_side,
op, new_category);
}
// static
CSSMathExpressionNode* CSSMathExpressionOperation::CreateComparisonFunction(
Operands&& operands,
CSSMathOperator op) {
DCHECK(op == CSSMathOperator::kMin || op == CSSMathOperator::kMax ||
op == CSSMathOperator::kClamp);
CalculationResultCategory category = DetermineComparisonCategory(operands);
if (category == kCalcOther) {
return nullptr;
}
return MakeGarbageCollected<CSSMathExpressionOperation>(
category, std::move(operands), op);
}
// static
CSSMathExpressionNode*
CSSMathExpressionOperation::CreateComparisonFunctionSimplified(
Operands&& operands,
CSSMathOperator op) {
DCHECK(op == CSSMathOperator::kMin || op == CSSMathOperator::kMax ||
op == CSSMathOperator::kClamp);
CalculationResultCategory category = DetermineComparisonCategory(operands);
if (category == kCalcOther) {
return nullptr;
}
if (CanEagerlySimplify(operands)) {
Vector<double> canonical_values;
canonical_values.reserve(operands.size());
for (const CSSMathExpressionNode* operand : operands) {
std::optional<double> canonical_value =
operand->ComputeValueInCanonicalUnit();
DCHECK(canonical_value.has_value());
canonical_values.push_back(canonical_value.value());
}
CSSPrimitiveValue::UnitType canonical_unit =
CSSPrimitiveValue::CanonicalUnit(operands.front()->ResolvedUnitType());
return CSSMathExpressionNumericLiteral::Create(
EvaluateOperator(canonical_values, op), canonical_unit);
}
if (operands.size() == 1) {
return operands.front()->Copy();
}
return MakeGarbageCollected<CSSMathExpressionOperation>(
category, std::move(operands), op);
}
// Helper function for parsing number value
static double ValueAsNumber(const CSSMathExpressionNode* node, bool& error) {
if (node->Category() == kCalcNumber) {
return node->DoubleValue();
}
error = true;
return 0;
}
static bool SupportedCategoryForAtan2(
const CalculationResultCategory category) {
switch (category) {
case kCalcNumber:
case kCalcLength:
case kCalcPercent:
case kCalcTime:
case kCalcFrequency:
case kCalcAngle:
return true;
default:
return false;
}
}
static bool IsRelativeLength(CSSPrimitiveValue::UnitType type) {
return CSSPrimitiveValue::IsRelativeUnit(type) &&
CSSPrimitiveValue::IsLength(type);
}
static double ResolveAtan2(const CSSMathExpressionNode* y_node,
const CSSMathExpressionNode* x_node,
bool& error) {
const CalculationResultCategory category = y_node->Category();
if (category != x_node->Category() || !SupportedCategoryForAtan2(category)) {
error = true;
return 0;
}
CSSPrimitiveValue::UnitType y_type = y_node->ResolvedUnitType();
CSSPrimitiveValue::UnitType x_type = x_node->ResolvedUnitType();
// TODO(crbug.com/1392594): We ignore parameters in complex relative units
// (e.g., 1rem + 1px) until they can be supported.
if (y_type == CSSPrimitiveValue::UnitType::kUnknown ||
x_type == CSSPrimitiveValue::UnitType::kUnknown) {
error = true;
return 0;
}
if (IsRelativeLength(y_type) || IsRelativeLength(x_type)) {
// TODO(crbug.com/1392594): Relative length units are currently hard
// to resolve. We ignore the units for now, so that
// we can at least support the case where both operands have the same unit.
double y = y_node->DoubleValue();
double x = x_node->DoubleValue();
return std::atan2(y, x);
}
auto y = y_node->ComputeValueInCanonicalUnit();
auto x = x_node->ComputeValueInCanonicalUnit();
return std::atan2(y.value(), x.value());
}
// Helper function for parsing trigonometric functions' parameter
static double ValueAsDegrees(const CSSMathExpressionNode* node, bool& error) {
if (node->Category() == kCalcAngle) {
return node->ComputeValueInCanonicalUnit().value();
}
return Rad2deg(ValueAsNumber(node, error));
}
CSSMathExpressionNode*
CSSMathExpressionOperation::CreateTrigonometricFunctionSimplified(
Operands&& operands,
CSSValueID function_id) {
double value;
auto unit_type = CSSPrimitiveValue::UnitType::kUnknown;
bool error = false;
switch (function_id) {
case CSSValueID::kSin: {
DCHECK_EQ(operands.size(), 1u);
unit_type = CSSPrimitiveValue::UnitType::kNumber;
value = gfx::SinCosDegrees(ValueAsDegrees(operands[0], error)).sin;
break;
}
case CSSValueID::kCos: {
DCHECK_EQ(operands.size(), 1u);
unit_type = CSSPrimitiveValue::UnitType::kNumber;
value = gfx::SinCosDegrees(ValueAsDegrees(operands[0], error)).cos;
break;
}
case CSSValueID::kTan: {
DCHECK_EQ(operands.size(), 1u);
unit_type = CSSPrimitiveValue::UnitType::kNumber;
value = TanDegrees(ValueAsDegrees(operands[0], error));
break;
}
case CSSValueID::kAsin: {
DCHECK_EQ(operands.size(), 1u);
unit_type = CSSPrimitiveValue::UnitType::kDegrees;
value = Rad2deg(std::asin(ValueAsNumber(operands[0], error)));
DCHECK(value >= -90 && value <= 90 || std::isnan(value));
break;
}
case CSSValueID::kAcos: {
DCHECK_EQ(operands.size(), 1u);
unit_type = CSSPrimitiveValue::UnitType::kDegrees;
value = Rad2deg(std::acos(ValueAsNumber(operands[0], error)));
DCHECK(value >= 0 && value <= 180 || std::isnan(value));
break;
}
case CSSValueID::kAtan: {
DCHECK_EQ(operands.size(), 1u);
unit_type = CSSPrimitiveValue::UnitType::kDegrees;
value = Rad2deg(std::atan(ValueAsNumber(operands[0], error)));
DCHECK(value >= -90 && value <= 90 || std::isnan(value));
break;
}
case CSSValueID::kAtan2: {
DCHECK_EQ(operands.size(), 2u);
unit_type = CSSPrimitiveValue::UnitType::kDegrees;
value = Rad2deg(ResolveAtan2(operands[0], operands[1], error));
DCHECK(value >= -180 && value <= 180 || std::isnan(value));
break;
}
default:
return nullptr;
}
if (error) {
return nullptr;
}
DCHECK_NE(unit_type, CSSPrimitiveValue::UnitType::kUnknown);
return CSSMathExpressionNumericLiteral::Create(value, unit_type);
}
CSSMathExpressionNode* CSSMathExpressionOperation::CreateSteppedValueFunction(
Operands&& operands,
CSSMathOperator op) {
if (!RuntimeEnabledFeatures::CSSSteppedValueFunctionsEnabled()) {
return nullptr;
}
DCHECK_EQ(operands.size(), 2u);
if (operands[0]->Category() == kCalcOther ||
operands[1]->Category() == kCalcOther) {
return nullptr;
}
CalculationResultCategory category =
kAddSubtractResult[operands[0]->Category()][operands[1]->Category()];
if (category == kCalcOther) {
return nullptr;
}
if (CanEagerlySimplify(operands)) {
std::optional<double> a = operands[0]->ComputeValueInCanonicalUnit();
std::optional<double> b = operands[1]->ComputeValueInCanonicalUnit();
DCHECK(a.has_value());
DCHECK(b.has_value());
double value = EvaluateSteppedValueFunction(op, a.value(), b.value());
return CSSMathExpressionNumericLiteral::Create(
value,
CSSPrimitiveValue::CanonicalUnit(operands.front()->ResolvedUnitType()));
}
return MakeGarbageCollected<CSSMathExpressionOperation>(
category, std::move(operands), op);
}
// static
CSSMathExpressionNode* CSSMathExpressionOperation::CreateExponentialFunction(
Operands&& operands,
CSSValueID function_id) {
if (!RuntimeEnabledFeatures::CSSExponentialFunctionsEnabled()) {
return nullptr;
}
double value = 0;
bool error = false;
auto unit_type = CSSPrimitiveValue::UnitType::kNumber;
switch (function_id) {
case CSSValueID::kPow: {
DCHECK_EQ(operands.size(), 2u);
double a = ValueAsNumber(operands[0], error);
double b = ValueAsNumber(operands[1], error);
value = std::pow(a, b);
break;
}
case CSSValueID::kSqrt: {
DCHECK_EQ(operands.size(), 1u);
double a = ValueAsNumber(operands[0], error);
value = std::sqrt(a);
break;
}
case CSSValueID::kHypot: {
DCHECK_GE(operands.size(), 1u);
CalculationResultCategory category =
DetermineComparisonCategory(operands);
if (category == kCalcOther) {
return nullptr;
}
if (CanEagerlySimplify(operands)) {
for (const CSSMathExpressionNode* operand : operands) {
std::optional<double> a = operand->ComputeValueInCanonicalUnit();
DCHECK(a.has_value());
value = std::hypot(value, a.value());
}
unit_type = CSSPrimitiveValue::CanonicalUnit(
operands.front()->ResolvedUnitType());
} else {
return MakeGarbageCollected<CSSMathExpressionOperation>(
category, std::move(operands), CSSMathOperator::kHypot);
}
break;
}
case CSSValueID::kLog: {
DCHECK_GE(operands.size(), 1u);
DCHECK_LE(operands.size(), 2u);
double a = ValueAsNumber(operands[0], error);
if (operands.size() == 2) {
double b = ValueAsNumber(operands[1], error);
value = std::log2(a) / std::log2(b);
} else {
value = std::log(a);
}
break;
}
case CSSValueID::kExp: {
DCHECK_EQ(operands.size(), 1u);
double a = ValueAsNumber(operands[0], error);
value = std::exp(a);
break;
}
default:
return nullptr;
}
if (error) {
return nullptr;
}
DCHECK_NE(unit_type, CSSPrimitiveValue::UnitType::kUnknown);
return CSSMathExpressionNumericLiteral::Create(value, unit_type);
}
CSSMathExpressionNode* CSSMathExpressionOperation::CreateSignRelatedFunction(
Operands&& operands,
CSSValueID function_id) {
if (!RuntimeEnabledFeatures::CSSSignRelatedFunctionsEnabled()) {
return nullptr;
}
const CSSMathExpressionNode* operand = operands.front();
if (operand->Category() == kCalcIntrinsicSize) {
return nullptr;
}
switch (function_id) {
case CSSValueID::kAbs: {
if (CanEagerlySimplify(operand)) {
const std::optional<double> opt =
operand->ComputeValueInCanonicalUnit();
DCHECK(opt.has_value());
return CSSMathExpressionNumericLiteral::Create(
std::abs(opt.value()), operand->ResolvedUnitType());
}
return MakeGarbageCollected<CSSMathExpressionOperation>(
operand->Category(), std::move(operands), CSSMathOperator::kAbs);
}
case CSSValueID::kSign: {
if (CanEagerlySimplify(operand)) {
const std::optional<double> opt =
operand->ComputeValueInCanonicalUnit();
DCHECK(opt.has_value());
const double value = opt.value();
const double signum =
(value == 0 || std::isnan(value)) ? value : ((value > 0) ? 1 : -1);
return CSSMathExpressionNumericLiteral::Create(
signum, CSSPrimitiveValue::UnitType::kNumber);
}
return MakeGarbageCollected<CSSMathExpressionOperation>(
kCalcNumber, std::move(operands), CSSMathOperator::kSign);
}
default:
NOTREACHED();
return nullptr;
}
}
inline const CSSMathExpressionOperation* DynamicToCalcSize(
const CSSMathExpressionNode* node) {
const CSSMathExpressionOperation* operation =
DynamicTo<CSSMathExpressionOperation>(node);
if (!operation || !operation->IsCalcSize()) {
return nullptr;
}
return operation;
}
// static
CSSMathExpressionNode*
CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
const CSSMathExpressionNode* left_side,
const CSSMathExpressionNode* right_side,
CSSMathOperator op) {
DCHECK(op == CSSMathOperator::kAdd || op == CSSMathOperator::kSubtract ||
op == CSSMathOperator::kMultiply || op == CSSMathOperator::kDivide);
if (CSSMathExpressionNode* result =
MaybeDistributeArithmeticOperation(left_side, right_side, op)) {
return result;
}
if (left_side->IsOperation() || right_side->IsOperation()) {
return CreateArithmeticOperation(left_side, right_side, op);
}
CalculationResultCategory left_category = left_side->Category();
CalculationResultCategory right_category = right_side->Category();
DCHECK_NE(left_category, kCalcOther);
DCHECK_NE(right_category, kCalcOther);
// Simplify numbers.
if (left_category == kCalcNumber && left_side->IsNumericLiteral() &&
right_category == kCalcNumber && right_side->IsNumericLiteral()) {
return CSSMathExpressionNumericLiteral::Create(
EvaluateOperator({left_side->DoubleValue(), right_side->DoubleValue()},
op),
CSSPrimitiveValue::UnitType::kNumber);
}
// 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);
}
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 =
left_side->DoubleValue() *
CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor(
left_type);
double right_value =
right_side->DoubleValue() *
CSSPrimitiveValue::ConversionToCanonicalUnitsScaleFactor(
right_type);
return CSSMathExpressionNumericLiteral::Create(
EvaluateOperator({left_value, right_value}, op),
canonical_type);
}
}
}
}
} else {
// Simplify multiplying or dividing by a number for simplifiable types.
DCHECK(op == CSSMathOperator::kMultiply || op == CSSMathOperator::kDivide);
const CSSMathExpressionNode* number_side =
GetNumericLiteralSide(left_side, right_side);
if (!number_side) {
return CreateArithmeticOperation(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();
CSSPrimitiveValue::UnitType other_type = other_side->ResolvedUnitType();
if (HasDoubleValue(other_type)) {
return CSSMathExpressionNumericLiteral::Create(
EvaluateOperator({other_side->DoubleValue(), number}, op),
other_type);
}
}
return CreateArithmeticOperation(left_side, right_side, op);
}
// static
CSSMathExpressionNode*
CSSMathExpressionOperation::CreateArithmeticOperationAndSimplifyCalcSize(
const CSSMathExpressionNode* left_side,
const CSSMathExpressionNode* right_side,
CSSMathOperator op) {
DCHECK(op == CSSMathOperator::kAdd || op == CSSMathOperator::kSubtract ||
op == CSSMathOperator::kMultiply || op == CSSMathOperator::kDivide);
// Merge calc-size() expressions to keep calc-size() always at the top level.
const CSSMathExpressionOperation* left_calc_size =
DynamicToCalcSize(left_side);
const CSSMathExpressionOperation* right_calc_size =
DynamicToCalcSize(right_side);
if (left_calc_size) {
if (right_calc_size) {
if (op != CSSMathOperator::kAdd && op != CSSMathOperator::kSubtract) {
return nullptr;
}
const CSSMathExpressionNode* left_basis =
left_calc_size->GetOperands()[0];
const CSSMathExpressionNode* right_basis =
right_calc_size->GetOperands()[0];
const CSSMathExpressionNode* final_basis = left_basis;
// Require that the bases are equal, or that one of them is the
// any keyword.
// TODO(https://crbug.com/313072): We should also accept nested
// basis, that is, combining calc-size(calc-size(B, X), Y) with
// calc-size(B, Z). This requires substituting X for the
// occurrences of the 'size' keyword in Y. This nesting can be
// arbitrarily deep.
if (*left_basis != *right_basis) {
auto is_any_keyword = [](const CSSMathExpressionNode* node) -> bool {
const auto* literal =
DynamicTo<CSSMathExpressionKeywordLiteral>(node);
return literal && literal->GetValue() == CSSValueID::kAny;
};
if (is_any_keyword(left_basis)) {
final_basis = right_basis;
} else if (!is_any_keyword(right_basis)) {
return nullptr;
}
}
const CSSMathExpressionNode* left_calculation =
left_calc_size->GetOperands()[1];
const CSSMathExpressionNode* right_calculation =
right_calc_size->GetOperands()[1];
return CreateCalcSizeOperation(
final_basis, CreateArithmeticOperationSimplified(
left_calculation, right_calculation, op));
} else {
const CSSMathExpressionNode* left_basis =
left_calc_size->GetOperands()[0];
const CSSMathExpressionNode* left_calculation =
left_calc_size->GetOperands()[1];
return CreateCalcSizeOperation(
left_basis, CreateArithmeticOperationSimplified(left_calculation,
right_side, op));
}
} else if (right_calc_size) {
const CSSMathExpressionNode* right_basis =
right_calc_size->GetOperands()[0];
const CSSMathExpressionNode* right_calculation =
right_calc_size->GetOperands()[1];
return CreateCalcSizeOperation(
right_basis,
CreateArithmeticOperationSimplified(left_side, right_calculation, op));
}
return CreateArithmeticOperationSimplified(left_side, right_side, op);
}
CSSMathExpressionOperation::CSSMathExpressionOperation(
const CSSMathExpressionNode* left_side,
const CSSMathExpressionNode* right_side,
CSSMathOperator op,
CalculationResultCategory category)
: CSSMathExpressionNode(
category,
left_side->HasComparisons() || right_side->HasComparisons(),
!left_side->IsScopedValue() || !right_side->IsScopedValue()),
operands_({left_side, right_side}),
operator_(op) {}
bool CSSMathExpressionOperation::HasPercentage() const {
if (Category() == kCalcPercent) {
return true;
}
if (Category() != kCalcLengthFunction && Category() != kCalcIntrinsicSize) {
return false;
}
switch (operator_) {
case CSSMathOperator::kProgress:
return false;
case CSSMathOperator::kCalcSize:
DCHECK_EQ(operands_.size(), 2u);
return operands_[0]->HasPercentage();
default:
break;
}
for (const CSSMathExpressionNode* operand : operands_) {
if (operand->HasPercentage()) {
return true;
}
}
return false;
}
bool CSSMathExpressionOperation::InvolvesLayout() const {
if (Category() == kCalcPercent || Category() == kCalcLengthFunction) {
return true;
}
for (const CSSMathExpressionNode* operand : operands_) {
if (operand->InvolvesLayout()) {
return true;
}
}
return false;
}
bool CSSMathExpressionOperation::InvolvesAnchorQueries() const {
for (const CSSMathExpressionNode* operand : operands_) {
if (operand->InvolvesAnchorQueries()) {
return true;
}
}
return false;
}
static bool AnyOperandHasComparisons(
CSSMathExpressionOperation::Operands& operands) {
for (const CSSMathExpressionNode* operand : operands) {
if (operand->HasComparisons()) {
return true;
}
}
return false;
}
static bool AnyOperandNeedsTreeScopePopulation(
CSSMathExpressionOperation::Operands& operands) {
for (const CSSMathExpressionNode* operand : operands) {
if (!operand->IsScopedValue()) {
return true;
}
}
return false;
}
CSSMathExpressionOperation::CSSMathExpressionOperation(
CalculationResultCategory category,
Operands&& operands,
CSSMathOperator op)
: CSSMathExpressionNode(
category,
IsComparison(op) || AnyOperandHasComparisons(operands),
AnyOperandNeedsTreeScopePopulation(operands)),
operands_(std::move(operands)),
operator_(op) {}
CSSMathExpressionOperation::CSSMathExpressionOperation(
CalculationResultCategory category,
CSSMathOperator op)
: CSSMathExpressionNode(category,
IsComparison(op),
false),
operator_(op) {}
bool CSSMathExpressionOperation::IsZero() const {
std::optional<double> maybe_value = ComputeValueInCanonicalUnit();
return maybe_value && !*maybe_value;
}
std::optional<PixelsAndPercent> CSSMathExpressionOperation::ToPixelsAndPercent(
const CSSLengthResolver& length_resolver) const {
std::optional<PixelsAndPercent> result;
switch (operator_) {
case CSSMathOperator::kAdd:
case CSSMathOperator::kSubtract: {
DCHECK_EQ(operands_.size(), 2u);
result = operands_[0]->ToPixelsAndPercent(length_resolver);
if (!result) {
return std::nullopt;
}
std::optional<PixelsAndPercent> other_side =
operands_[1]->ToPixelsAndPercent(length_resolver);
if (!other_side) {
return std::nullopt;
}
if (operator_ == CSSMathOperator::kAdd) {
result.value() += other_side.value();
} else {
result.value() -= other_side.value();
}
break;
}
case CSSMathOperator::kMultiply:
case CSSMathOperator::kDivide: {
DCHECK_EQ(operands_.size(), 2u);
const CSSMathExpressionNode* number_side =
GetNumericLiteralSide(operands_[0], operands_[1]);
if (!number_side) {
return std::nullopt;
}
const CSSMathExpressionNode* other_side =
operands_[0] == number_side ? operands_[1] : operands_[0];
result = other_side->ToPixelsAndPercent(length_resolver);
if (!result) {
return std::nullopt;
}
float number = number_side->DoubleValue();
if (operator_ == CSSMathOperator::kDivide) {
number = 1.0 / number;
}
result.value() *= number;
break;
}
case CSSMathOperator::kCalcSize:
// While it looks like we might be able to handle some calc-size() cases
// here, we don't want to do because it would be difficult to avoid a
// has_explicit_percent state inside the calculation propagating to the
// result (which should not happen; only the has_explicit_percent state
// from the basis should do so).
return std::nullopt;
case CSSMathOperator::kMin:
case CSSMathOperator::kMax:
case CSSMathOperator::kClamp:
case CSSMathOperator::kRoundNearest:
case CSSMathOperator::kRoundUp:
case CSSMathOperator::kRoundDown:
case CSSMathOperator::kRoundToZero:
case CSSMathOperator::kMod:
case CSSMathOperator::kRem:
case CSSMathOperator::kHypot:
case CSSMathOperator::kAbs:
case CSSMathOperator::kSign:
case CSSMathOperator::kProgress:
case CSSMathOperator::kMediaProgress:
return std::nullopt;
case CSSMathOperator::kInvalid:
NOTREACHED();
}
return result;
}
scoped_refptr<const CalculationExpressionNode>
CSSMathExpressionOperation::ToCalculationExpression(
const CSSLengthResolver& length_resolver) const {
switch (operator_) {
case CSSMathOperator::kAdd:
DCHECK_EQ(operands_.size(), 2u);
return CalculationExpressionOperationNode::CreateSimplified(
CalculationExpressionOperationNode::Children(
{operands_[0]->ToCalculationExpression(length_resolver),
operands_[1]->ToCalculationExpression(length_resolver)}),
CalculationOperator::kAdd);
case CSSMathOperator::kSubtract:
DCHECK_EQ(operands_.size(), 2u);
return CalculationExpressionOperationNode::CreateSimplified(
CalculationExpressionOperationNode::Children(
{operands_[0]->ToCalculationExpression(length_resolver),
operands_[1]->ToCalculationExpression(length_resolver)}),
CalculationOperator::kSubtract);
case CSSMathOperator::kMultiply:
DCHECK_EQ(operands_.size(), 2u);
return CalculationExpressionOperationNode::CreateSimplified(
{operands_.front()->ToCalculationExpression(length_resolver),
operands_.back()->ToCalculationExpression(length_resolver)},
CalculationOperator::kMultiply);
case CSSMathOperator::kDivide:
DCHECK_EQ(operands_.size(), 2u);
DCHECK_EQ(operands_[1]->Category(), kCalcNumber);
return CalculationExpressionOperationNode::CreateSimplified(
CalculationExpressionOperationNode::Children(
{operands_[0]->ToCalculationExpression(length_resolver),
base::MakeRefCounted<CalculationExpressionNumberNode>(
1.0 / operands_[1]->DoubleValue())}),
CalculationOperator::kMultiply);
case CSSMathOperator::kMin:
case CSSMathOperator::kMax: {
Vector<scoped_refptr<const CalculationExpressionNode>> operands;
operands.reserve(operands_.size());
for (const CSSMathExpressionNode* operand : operands_) {
operands.push_back(operand->ToCalculationExpression(length_resolver));
}
auto expression_operator = operator_ == CSSMathOperator::kMin
? CalculationOperator::kMin
: CalculationOperator::kMax;
return CalculationExpressionOperationNode::CreateSimplified(
std::move(operands), expression_operator);
}
case CSSMathOperator::kClamp: {
Vector<scoped_refptr<const CalculationExpressionNode>> operands;
operands.reserve(operands_.size());
for (const CSSMathExpressionNode* operand : operands_) {
operands.push_back(operand->ToCalculationExpression(length_resolver));
}
return CalculationExpressionOperationNode::CreateSimplified(
std::move(operands), CalculationOperator::kClamp);
}
case CSSMathOperator::kRoundNearest:
case CSSMathOperator::kRoundUp:
case CSSMathOperator::kRoundDown:
case CSSMathOperator::kRoundToZero:
case CSSMathOperator::kMod:
case CSSMathOperator::kRem:
case CSSMathOperator::kHypot:
case CSSMathOperator::kAbs:
case CSSMathOperator::kSign:
case CSSMathOperator::kProgress:
case CSSMathOperator::kMediaProgress:
case CSSMathOperator::kCalcSize: {
Vector<scoped_refptr<const CalculationExpressionNode>> operands;
operands.reserve(operands_.size());
for (const CSSMathExpressionNode* operand : operands_) {
operands.push_back(operand->ToCalculationExpression(length_resolver));
}
CalculationOperator op;
if (operator_ == CSSMathOperator::kRoundNearest) {
op = CalculationOperator::kRoundNearest;
} else if (operator_ == CSSMathOperator::kRoundUp) {
op = CalculationOperator::kRoundUp;
} else if (operator_ == CSSMathOperator::kRoundDown) {
op = CalculationOperator::kRoundDown;
} else if (operator_ == CSSMathOperator::kRoundToZero) {
op = CalculationOperator::kRoundToZero;
} else if (operator_ == CSSMathOperator::kMod) {
op = CalculationOperator::kMod;
} else if (operator_ == CSSMathOperator::kRem) {
op = CalculationOperator::kRem;
} else if (operator_ == CSSMathOperator::kHypot) {
op = CalculationOperator::kHypot;
} else if (operator_ == CSSMathOperator::kAbs) {
op = CalculationOperator::kAbs;
} else if (operator_ == CSSMathOperator::kSign) {
op = CalculationOperator::kSign;
} else if (operator_ == CSSMathOperator::kProgress) {
op = CalculationOperator::kProgress;
} else if (operator_ == CSSMathOperator::kMediaProgress) {
op = CalculationOperator::kMediaProgress;
} else {
CHECK(operator_ == CSSMathOperator::kCalcSize);
op = CalculationOperator::kCalcSize;
}
return CalculationExpressionOperationNode::CreateSimplified(
std::move(operands), op);
}
case CSSMathOperator::kInvalid:
NOTREACHED();
return nullptr;
}
}
double CSSMathExpressionOperation::DoubleValue() const {
DCHECK(HasDoubleValue(ResolvedUnitType())) << CustomCSSText();
Vector<double> double_values;
double_values.reserve(operands_.size());
for (const CSSMathExpressionNode* operand : operands_) {
double_values.push_back(operand->DoubleValue());
}
return Evaluate(double_values);
}
static bool HasCanonicalUnit(CalculationResultCategory category) {
return category == kCalcNumber || category == kCalcLength ||
category == kCalcPercent || category == kCalcAngle ||
category == kCalcTime || category == kCalcFrequency ||
category == kCalcResolution;
}
std::optional<double> CSSMathExpressionOperation::ComputeValueInCanonicalUnit()
const {
if (!HasCanonicalUnit(category_)) {
return std::nullopt;
}
Vector<double> double_values;
double_values.reserve(operands_.size());
for (const CSSMathExpressionNode* operand : operands_) {
std::optional<double> maybe_value = operand->ComputeValueInCanonicalUnit();
if (!maybe_value) {
return std::nullopt;
}
double_values.push_back(*maybe_value);
}
return Evaluate(double_values);
}
double CSSMathExpressionOperation::ComputeDouble(
const CSSLengthResolver& length_resolver) const {
Vector<double> double_values;
double_values.reserve(operands_.size());
for (const CSSMathExpressionNode* operand : operands_) {
double_values.push_back(
CSSMathExpressionNode::ComputeDouble(operand, length_resolver));
}
return Evaluate(double_values);
}
double CSSMathExpressionOperation::ComputeLengthPx(
const CSSLengthResolver& length_resolver) const {
DCHECK(!HasPercentage());
DCHECK_EQ(Category(), kCalcLength);
return ComputeDouble(length_resolver);
}
bool CSSMathExpressionOperation::AccumulateLengthArray(
CSSLengthArray& length_array,
double multiplier) const {
switch (operator_) {
case CSSMathOperator::kAdd:
DCHECK_EQ(operands_.size(), 2u);
if (!operands_[0]->AccumulateLengthArray(length_array, multiplier)) {
return false;
}
if (!operands_[1]->AccumulateLengthArray(length_array, multiplier)) {
return false;
}
return true;
case CSSMathOperator::kSubtract:
DCHECK_EQ(operands_.size(), 2u);
if (!operands_[0]->AccumulateLengthArray(length_array, multiplier)) {
return false;
}
if (!operands_[1]->AccumulateLengthArray(length_array, -multiplier)) {
return false;
}
return true;
case CSSMathOperator::kMultiply:
DCHECK_EQ(operands_.size(), 2u);
DCHECK_NE((operands_[0]->Category() == kCalcNumber),
(operands_[1]->Category() == kCalcNumber));
if (operands_[0]->Category() == kCalcNumber) {
return operands_[1]->AccumulateLengthArray(
length_array, multiplier * operands_[0]->DoubleValue());
} else {
return operands_[0]->AccumulateLengthArray(
length_array, multiplier * operands_[1]->DoubleValue());
}
case CSSMathOperator::kDivide:
DCHECK_EQ(operands_.size(), 2u);
DCHECK_EQ(operands_[1]->Category(), kCalcNumber);
return operands_[0]->AccumulateLengthArray(
length_array, multiplier / operands_[1]->DoubleValue());
case CSSMathOperator::kMin:
case CSSMathOperator::kMax:
case CSSMathOperator::kClamp:
// When comparison functions are involved, we can't resolve the expression
// into a length array.
case CSSMathOperator::kRoundNearest:
case CSSMathOperator::kRoundUp:
case CSSMathOperator::kRoundDown:
case CSSMathOperator::kRoundToZero:
case CSSMathOperator::kMod:
case CSSMathOperator::kRem:
case CSSMathOperator::kHypot:
case CSSMathOperator::kAbs:
case CSSMathOperator::kSign:
// When stepped value functions are involved, we can't resolve the
// expression into a length array.
case CSSMathOperator::kProgress:
case CSSMathOperator::kCalcSize:
case CSSMathOperator::kMediaProgress:
return false;
case CSSMathOperator::kInvalid:
NOTREACHED();
return false;
}
}
void CSSMathExpressionOperation::AccumulateLengthUnitTypes(
CSSPrimitiveValue::LengthTypeFlags& types) const {
for (const CSSMathExpressionNode* operand : operands_) {
operand->AccumulateLengthUnitTypes(types);
}
}
bool CSSMathExpressionOperation::IsComputationallyIndependent() const {
for (const CSSMathExpressionNode* operand : operands_) {
if (!operand->IsComputationallyIndependent()) {
return false;
}
}
return true;
}
String CSSMathExpressionOperation::CustomCSSText() const {
switch (operator_) {
case CSSMathOperator::kAdd:
case CSSMathOperator::kSubtract:
case CSSMathOperator::kMultiply:
case CSSMathOperator::kDivide: {
DCHECK_EQ(operands_.size(), 2u);
// As per
// https://drafts.csswg.org/css-values-4/#sort-a-calculations-children
// we should sort the dimensions of the sum node.
const CSSMathExpressionOperation* operation = this;
if (IsAddOrSubtract()) {
const CSSMathExpressionNode* node = MaybeSortSumNode(this);
// Note: we can hit here, since CSS Typed OM doesn't currently follow
// the same simplifications as CSS Values spec.
// https://github.com/w3c/csswg-drafts/issues/9451
if (!node->IsOperation()) {
return node->CustomCSSText();
}
operation = To<CSSMathExpressionOperation>(node);
}
CSSMathOperator op = operation->OperatorType();
const Operands& operands = operation->GetOperands();
StringBuilder result;
// After all the simplifications we only need parentheses here for the
// cases like: (lhs as unsimplified sum/sub) [* or /] rhs
const bool left_side_needs_parentheses =
IsMultiplyOrDivide() && operands.front()->IsOperation() &&
To<CSSMathExpressionOperation>(operands.front().Get())
->IsAddOrSubtract();
if (left_side_needs_parentheses) {
result.Append('(');
}
result.Append(operands[0]->CustomCSSText());
if (left_side_needs_parentheses) {
result.Append(')');
}
result.Append(' ');
result.Append(ToString(op));
result.Append(' ');
// After all the simplifications we only need parentheses here for the
// cases like: lhs [* or /] (rhs as unsimplified sum/sub)
const bool right_side_needs_parentheses =
IsMultiplyOrDivide() && operands.back()->IsOperation() &&
To<CSSMathExpressionOperation>(operands.back().Get())
->IsAddOrSubtract();
if (right_side_needs_parentheses) {
result.Append('(');
}
result.Append(operands[1]->CustomCSSText());
if (right_side_needs_parentheses) {
result.Append(')');
}
return result.ReleaseString();
}
case CSSMathOperator::kMin:
case CSSMathOperator::kMax:
case CSSMathOperator::kClamp:
case CSSMathOperator::kRoundNearest:
case CSSMathOperator::kMod:
case CSSMathOperator::kRem:
case CSSMathOperator::kHypot:
case CSSMathOperator::kAbs:
case CSSMathOperator::kSign:
case CSSMathOperator::kCalcSize: {
StringBuilder result;
result.Append(ToString(operator_));
result.Append('(');
result.Append(operands_.front()->CustomCSSText());
for (const CSSMathExpressionNode* operand : SecondToLastOperands()) {
result.Append(", ");
result.Append(operand->CustomCSSText());
}
result.Append(')');
return result.ReleaseString();
}
case CSSMathOperator::kRoundUp:
case CSSMathOperator::kRoundDown:
case CSSMathOperator::kRoundToZero: {
StringBuilder result;
result.Append(ToString(operator_));
result.Append(operands_.front()->CustomCSSText());
for (const CSSMathExpressionNode* operand : SecondToLastOperands()) {
result.Append(", ");
result.Append(operand->CustomCSSText());
}
result.Append(')');
return result.ReleaseString();
}
case CSSMathOperator::kProgress:
case CSSMathOperator::kMediaProgress: {
CHECK_EQ(operands_.size(), 3u);
StringBuilder result;
result.Append(ToString(operator_));
result.Append('(');
result.Append(operands_.front()->CustomCSSText());
result.Append(" from ");
result.Append(operands_[1]->CustomCSSText());
result.Append(" to ");
result.Append(operands_.back()->CustomCSSText());
result.Append(')');
return result.ReleaseString();
}
case CSSMathOperator::kInvalid:
NOTREACHED();
return String();
}
}
bool CSSMathExpressionOperation::operator==(
const CSSMathExpressionNode& exp) const {
if (!exp.IsOperation()) {
return false;
}
const CSSMathExpressionOperation& other = To<CSSMathExpressionOperation>(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 (!base::ValuesEquivalent(operands_[i], other.operands_[i])) {
return false;
}
}
return true;
}
CSSPrimitiveValue::UnitType CSSMathExpressionOperation::ResolvedUnitType()
const {
switch (category_) {
case kCalcNumber:
return CSSPrimitiveValue::UnitType::kNumber;
case kCalcAngle:
case kCalcTime:
case kCalcFrequency:
case kCalcLength:
case kCalcPercent:
case kCalcResolution:
switch (operator_) {
case CSSMathOperator::kMultiply:
case CSSMathOperator::kDivide: {
DCHECK_EQ(operands_.size(), 2u);
if (operands_[0]->Category() == kCalcNumber) {
return operands_[1]->ResolvedUnitType();
}
if (operands_[1]->Category() == kCalcNumber) {
return operands_[0]->ResolvedUnitType();
}
NOTREACHED();
return CSSPrimitiveValue::UnitType::kUnknown;
}
case CSSMathOperator::kAdd:
case CSSMathOperator::kSubtract:
case CSSMathOperator::kMin:
case CSSMathOperator::kMax:
case CSSMathOperator::kClamp:
case CSSMathOperator::kRoundNearest:
case CSSMathOperator::kRoundUp:
case CSSMathOperator::kRoundDown:
case CSSMathOperator::kRoundToZero:
case CSSMathOperator::kMod:
case CSSMathOperator::kRem:
case CSSMathOperator::kHypot:
case CSSMathOperator::kAbs: {
CSSPrimitiveValue::UnitType first_type =
operands_.front()->ResolvedUnitType();
if (first_type == CSSPrimitiveValue::UnitType::kUnknown) {
return CSSPrimitiveValue::UnitType::kUnknown;
}
for (const CSSMathExpressionNode* operand : SecondToLastOperands()) {
CSSPrimitiveValue::UnitType next = operand->ResolvedUnitType();
if (next == CSSPrimitiveValue::UnitType::kUnknown ||
next != first_type) {
return CSSPrimitiveValue::UnitType::kUnknown;
}
}
return first_type;
}
case CSSMathOperator::kSign:
case CSSMathOperator::kProgress:
case CSSMathOperator::kMediaProgress:
return CSSPrimitiveValue::UnitType::kNumber;
case CSSMathOperator::kCalcSize: {
DCHECK_EQ(operands_.size(), 2u);
CSSPrimitiveValue::UnitType calculation_type =
operands_[1]->ResolvedUnitType();
if (calculation_type != CSSPrimitiveValue::UnitType::kIdent) {
// The basis is not involved.
return calculation_type;
}
// TODO(https://crbug.com/313072): We could in theory resolve the
// 'size' keyword to produce a correct answer in more cases.
return CSSPrimitiveValue::UnitType::kUnknown;
}
case CSSMathOperator::kInvalid:
NOTREACHED();
return CSSPrimitiveValue::UnitType::kUnknown;
}
case kCalcLengthFunction:
case kCalcIntrinsicSize:
case kCalcOther:
return CSSPrimitiveValue::UnitType::kUnknown;
case kCalcIdent:
return CSSPrimitiveValue::UnitType::kIdent;
}
NOTREACHED();
return CSSPrimitiveValue::UnitType::kUnknown;
}
void CSSMathExpressionOperation::Trace(Visitor* visitor) const {
visitor->Trace(operands_);
CSSMathExpressionNode::Trace(visitor);
}
// static
const CSSMathExpressionNode* CSSMathExpressionOperation::GetNumericLiteralSide(
const CSSMathExpressionNode* left_side,
const CSSMathExpressionNode* right_side) {
if (left_side->Category() == kCalcNumber && left_side->IsNumericLiteral()) {
return left_side;
}
if (right_side->Category() == kCalcNumber && right_side->IsNumericLiteral()) {
return right_side;
}
return nullptr;
}
// static
double CSSMathExpressionOperation::EvaluateOperator(
const Vector<double>& operands,
CSSMathOperator op) {
// Design doc for infinity and NaN: https://bit.ly/349gXjq
// Any operation with at least one NaN argument produces NaN
// https://drafts.csswg.org/css-values/#calc-type-checking
for (double operand : operands) {
if (std::isnan(operand)) {
return operand;
}
}
switch (op) {
case CSSMathOperator::kAdd:
DCHECK_EQ(operands.size(), 2u);
return operands[0] + operands[1];
case CSSMathOperator::kSubtract:
DCHECK_EQ(operands.size(), 2u);
return operands[0] - operands[1];
case CSSMathOperator::kMultiply:
DCHECK_EQ(operands.size(), 2u);
return operands[0] * operands[1];
case CSSMathOperator::kDivide:
DCHECK(operands.size() == 1u || operands.size() == 2u);
return operands[0] / operands[1];
case CSSMathOperator::kMin: {
if (operands.empty()) {
return std::numeric_limits<double>::quiet_NaN();
}
double minimum = operands[0];
for (double operand : operands) {
minimum = std::min(minimum, operand);
}
return minimum;
}
case CSSMathOperator::kMax: {
if (operands.empty()) {
return std::numeric_limits<double>::quiet_NaN();
}
double maximum = operands[0];
for (double operand : operands) {
maximum = std::max(maximum, operand);
}
return maximum;
}
case CSSMathOperator::kClamp: {
DCHECK_EQ(operands.size(), 3u);
double min = operands[0];
double val = operands[1];
double max = operands[2];
// clamp(MIN, VAL, MAX) is identical to max(MIN, min(VAL, MAX))
// according to the spec,
// https://drafts.csswg.org/css-values-4/#funcdef-clamp.
return std::max(min, std::min(val, max));
}
case CSSMathOperator::kRoundNearest:
case CSSMathOperator::kRoundUp:
case CSSMathOperator::kRoundDown:
case CSSMathOperator::kRoundToZero:
case CSSMathOperator::kMod:
case CSSMathOperator::kRem: {
DCHECK_EQ(operands.size(), 2u);
return EvaluateSteppedValueFunction(op, operands[0], operands[1]);
}
case CSSMathOperator::kHypot: {
DCHECK_GE(operands.size(), 1u);
double value = 0;
for (double operand : operands) {
value = std::hypot(value, operand);
}
return value;
}
case CSSMathOperator::kAbs: {
DCHECK_EQ(operands.size(), 1u);
return std::abs(operands.front());
}
case CSSMathOperator::kSign: {
DCHECK_EQ(operands.size(), 1u);
const double value = operands.front();
const double signum =
(value == 0 || std::isnan(value)) ? value : ((value > 0) ? 1 : -1);
return signum;
}
case CSSMathOperator::kProgress:
case CSSMathOperator::kMediaProgress: {
CHECK_EQ(operands.size(), 3u);
return (operands[0] - operands[1]) / (operands[2] - operands[1]);
}
case CSSMathOperator::kCalcSize: {
CHECK_EQ(operands.size(), 2u);
// TODO(https://crbug.com/313072): In theory we could also
// evaluate (a) cases where the basis (operand 0) is not a double,
// and (b) cases where the basis (operand 0) is a double and the
// calculation (operand 1) requires 'size' keyword substitutions.
// But for now just handle the simplest case.
return operands[1];
}
case CSSMathOperator::kInvalid:
NOTREACHED();
break;
}
return 0;
}
const CSSMathExpressionNode& CSSMathExpressionOperation::PopulateWithTreeScope(
const TreeScope* tree_scope) const {
Operands populated_operands;
for (const CSSMathExpressionNode* op : operands_) {
populated_operands.push_back(&op->EnsureScopedValue(tree_scope));
}
return *MakeGarbageCollected<CSSMathExpressionOperation>(
Category(), std::move(populated_operands), operator_);
}
const CSSMathExpressionNode* CSSMathExpressionOperation::TransformAnchors(
LogicalAxis logical_axis,
const TryTacticTransform& transform,
const WritingDirectionMode& writing_direction) const {
Operands transformed_operands;
for (const CSSMathExpressionNode* op : operands_) {
transformed_operands.push_back(
op->TransformAnchors(logical_axis, transform, writing_direction));
}
if (transformed_operands != operands_) {
return MakeGarbageCollected<CSSMathExpressionOperation>(
Category(), std::move(transformed_operands), operator_);
}
return this;
}
#if DCHECK_IS_ON()
bool CSSMathExpressionOperation::InvolvesPercentageComparisons() const {
if (IsMinOrMax() && Category() == kCalcPercent && operands_.size() > 1u) {
return true;
}
for (const CSSMathExpressionNode* operand : operands_) {
if (operand->InvolvesPercentageComparisons()) {
return true;
}
}
return false;
}
#endif
// ------ End of CSSMathExpressionOperation member functions ------
// ------ Start of CSSMathExpressionAnchorQuery member functions ------
namespace {
CalculationResultCategory AnchorQueryCategory(
const CSSPrimitiveValue* fallback) {
// Note that the main (non-fallback) result of an anchor query is always
// a kCalcLength, so the only thing that can make our overall result anything
// else is the fallback.
if (!fallback || fallback->IsLength()) {
return kCalcLength;
}
// This can happen for e.g. anchor(--a top, 10%). In this case, we can't
// tell if we're going to return a <length> or a <percentage> without actually
// evaluating the query.
//
// TODO(crbug.com/326088870): Evaluate anchor queries when understanding
// the CalculationResultCategory for an expression.
return kCalcLengthFunction;
}
} // namespace
CSSMathExpressionAnchorQuery::CSSMathExpressionAnchorQuery(
CSSAnchorQueryType type,
const CSSValue* anchor_specifier,
const CSSValue& value,
const CSSPrimitiveValue* fallback)
: CSSMathExpressionNode(
AnchorQueryCategory(fallback),
false /* has_comparisons */,
(anchor_specifier && !anchor_specifier->IsScopedValue()) ||
(fallback && !fallback->IsScopedValue())),
type_(type),
anchor_specifier_(anchor_specifier),
value_(value),
fallback_(fallback) {}
double CSSMathExpressionAnchorQuery::DoubleValue() const {
NOTREACHED();
return 0;
}
double CSSMathExpressionAnchorQuery::ComputeLengthPx(
const CSSLengthResolver& length_resolver) const {
return ComputeDouble(length_resolver);
}
double CSSMathExpressionAnchorQuery::ComputeDouble(
const CSSLengthResolver& length_resolver) const {
CHECK_EQ(kCalcLength, Category());
// Note: The category may also be kCalcLengthFunction (see
// AnchorQueryCategory), in which case we'll reach ToCalculationExpression
// instead.
AnchorQuery query = ToQuery(length_resolver);
if (std::optional<LayoutUnit> px = EvaluateQuery(query, length_resolver)) {
return px.value();
}
return fallback_ ? fallback_->ComputeLength<double>(length_resolver) : 0;
}
String CSSMathExpressionAnchorQuery::CustomCSSText() const {
StringBuilder result;
result.Append(IsAnchor() ? "anchor(" : "anchor-size(");
if (anchor_specifier_) {
result.Append(anchor_specifier_->CssText());
result.Append(" ");
}
result.Append(value_->CssText());
if (fallback_) {
result.Append(", ");
result.Append(fallback_->CustomCSSText());
}
result.Append(")");
return result.ToString();
}
bool CSSMathExpressionAnchorQuery::operator==(
const CSSMathExpressionNode& other) const {
const auto* other_anchor = DynamicTo<CSSMathExpressionAnchorQuery>(other);
if (!other_anchor) {
return false;
}
return type_ == other_anchor->type_ &&
base::ValuesEquivalent(anchor_specifier_,
other_anchor->anchor_specifier_) &&
base::ValuesEquivalent(value_, other_anchor->value_) &&
base::ValuesEquivalent(fallback_, other_anchor->fallback_);
}
namespace {
CSSAnchorValue CSSValueIDToAnchorValueEnum(CSSValueID value) {
switch (value) {
case CSSValueID::kTop:
return CSSAnchorValue::kTop;
case CSSValueID::kLeft:
return CSSAnchorValue::kLeft;
case CSSValueID::kRight:
return CSSAnchorValue::kRight;
case CSSValueID::kBottom:
return CSSAnchorValue::kBottom;
case CSSValueID::kStart:
return CSSAnchorValue::kStart;
case CSSValueID::kEnd:
return CSSAnchorValue::kEnd;
case CSSValueID::kSelfStart:
return CSSAnchorValue::kSelfStart;
case CSSValueID::kSelfEnd:
return CSSAnchorValue::kSelfEnd;
case CSSValueID::kCenter:
return CSSAnchorValue::kCenter;
default:
NOTREACHED();
return CSSAnchorValue::kCenter;
}
}
CSSAnchorSizeValue CSSValueIDToAnchorSizeValueEnum(CSSValueID value) {
switch (value) {
case CSSValueID::kWidth:
return CSSAnchorSizeValue::kWidth;
case CSSValueID::kHeight:
return CSSAnchorSizeValue::kHeight;
case CSSValueID::kBlock:
return CSSAnchorSizeValue::kBlock;
case CSSValueID::kInline:
return CSSAnchorSizeValue::kInline;
case CSSValueID::kSelfBlock:
return CSSAnchorSizeValue::kSelfBlock;
case CSSValueID::kSelfInline:
return CSSAnchorSizeValue::kSelfInline;
default:
NOTREACHED();
return CSSAnchorSizeValue::kSelfInline;
}
}
} // namespace
scoped_refptr<const CalculationExpressionNode>
CSSMathExpressionAnchorQuery::ToCalculationExpression(
const CSSLengthResolver& length_resolver) const {
AnchorQuery query = ToQuery(length_resolver);
Length result;
if (std::optional<LayoutUnit> px = EvaluateQuery(query, length_resolver)) {
result = Length::Fixed(px.value());
} else if (fallback_) {
result = fallback_->ConvertToLength(length_resolver);
} else {
result = Length::Fixed(0);
}
return result.AsCalculationValue()->GetOrCreateExpression();
}
std::optional<LayoutUnit> CSSMathExpressionAnchorQuery::EvaluateQuery(
const AnchorQuery& query,
const CSSLengthResolver& length_resolver) const {
length_resolver.ReferenceAnchor();
if (AnchorEvaluator* anchor_evaluator =
length_resolver.GetAnchorEvaluator()) {
return anchor_evaluator->Evaluate(query);
}
return std::nullopt;
}
AnchorQuery CSSMathExpressionAnchorQuery::ToQuery(
const CSSLengthResolver& length_resolver) const {
DCHECK(IsScopedValue());
AnchorSpecifierValue* anchor_specifier = AnchorSpecifierValue::Default();
if (const auto* implicit =
DynamicTo<CSSIdentifierValue>(anchor_specifier_.Get())) {
DCHECK_EQ(implicit->GetValueID(), CSSValueID::kImplicit);
anchor_specifier = AnchorSpecifierValue::Implicit();
} else if (const auto* custom_ident =
DynamicTo<CSSCustomIdentValue>(anchor_specifier_.Get())) {
length_resolver.ReferenceTreeScope();
anchor_specifier = MakeGarbageCollected<AnchorSpecifierValue>(
*MakeGarbageCollected<ScopedCSSName>(custom_ident->Value(),
custom_ident->GetTreeScope()));
}
if (type_ == CSSAnchorQueryType::kAnchor) {
if (const CSSPrimitiveValue* percentage =
DynamicTo<CSSPrimitiveValue>(*value_)) {
DCHECK(percentage->IsPercentage());
return AnchorQuery(type_, anchor_specifier, percentage->GetFloatValue(),
CSSAnchorValue::kPercentage);
}
const CSSIdentifierValue& side = To<CSSIdentifierValue>(*value_);
return AnchorQuery(type_, anchor_specifier, /* percentage */ 0,
CSSValueIDToAnchorValueEnum(side.GetValueID()));
}
DCHECK_EQ(type_, CSSAnchorQueryType::kAnchorSize);
const CSSIdentifierValue& size = To<CSSIdentifierValue>(*value_);
return AnchorQuery(type_, anchor_specifier, /* percentage */ 0,
CSSValueIDToAnchorSizeValueEnum(size.GetValueID()));
}
const CSSMathExpressionNode&
CSSMathExpressionAnchorQuery::PopulateWithTreeScope(
const TreeScope* tree_scope) const {
return *MakeGarbageCollected<CSSMathExpressionAnchorQuery>(
type_,
anchor_specifier_ ? &anchor_specifier_->EnsureScopedValue(tree_scope)
: nullptr,
*value_,
fallback_
? To<CSSPrimitiveValue>(&fallback_->EnsureScopedValue(tree_scope))
: nullptr);
}
namespace {
bool FlipLogical(LogicalAxis logical_axis,
const TryTacticTransform& transform) {
return (logical_axis == LogicalAxis::kInline) ? transform.FlippedInline()
: transform.FlippedBlock();
}
CSSValueID TransformAnchorCSSValueID(
CSSValueID from,
LogicalAxis logical_axis,
const TryTacticTransform& transform,
const WritingDirectionMode& writing_direction) {
// The value transformation happens on logical insets, so we need to first
// translate physical to logical, then carry out the transform, and then
// convert *back* to physical.
PhysicalToLogical logical_insets(writing_direction, CSSValueID::kTop,
CSSValueID::kRight, CSSValueID::kBottom,
CSSValueID::kLeft);
LogicalToPhysical<CSSValueID> insets = transform.Transform(
TryTacticTransform::LogicalSides<CSSValueID>{
.inline_start = logical_insets.InlineStart(),
.inline_end = logical_insets.InlineEnd(),
.block_start = logical_insets.BlockStart(),
.block_end = logical_insets.BlockEnd()},
writing_direction);
bool flip_logical = FlipLogical(logical_axis, transform);
switch (from) {
// anchor()
case CSSValueID::kTop:
return insets.Top();
case CSSValueID::kLeft:
return insets.Left();
case CSSValueID::kRight:
return insets.Right();
case CSSValueID::kBottom:
return insets.Bottom();
case CSSValueID::kStart:
return flip_logical ? CSSValueID::kEnd : from;
case CSSValueID::kEnd:
return flip_logical ? CSSValueID::kStart : from;
case CSSValueID::kSelfStart:
return flip_logical ? CSSValueID::kSelfEnd : from;
case CSSValueID::kSelfEnd:
return flip_logical ? CSSValueID::kSelfStart : from;
case CSSValueID::kCenter:
return from;
// anchor-size()
case CSSValueID::kWidth:
return transform.FlippedStart() ? CSSValueID::kHeight : from;
case CSSValueID::kHeight:
return transform.FlippedStart() ? CSSValueID::kWidth : from;
case CSSValueID::kBlock:
return transform.FlippedStart() ? CSSValueID::kInline : from;
case CSSValueID::kInline:
return transform.FlippedStart() ? CSSValueID::kBlock : from;
case CSSValueID::kSelfBlock:
return transform.FlippedStart() ? CSSValueID::kSelfInline : from;
case CSSValueID::kSelfInline:
return transform.FlippedStart() ? CSSValueID::kSelfBlock : from;
default:
NOTREACHED();
return from;
}
}
float TransformAnchorPercentage(float from,
LogicalAxis logical_axis,
const TryTacticTransform& transform) {
return FlipLogical(logical_axis, transform) ? (100.0f - from) : from;
}
} // namespace
const CSSMathExpressionNode* CSSMathExpressionAnchorQuery::TransformAnchors(
LogicalAxis logical_axis,
const TryTacticTransform& transform,
const WritingDirectionMode& writing_direction) const {
const CSSValue* transformed_value = value_;
if (const auto* side = DynamicTo<CSSIdentifierValue>(value_.Get())) {
CSSValueID from = side->GetValueID();
CSSValueID to = TransformAnchorCSSValueID(from, logical_axis, transform,
writing_direction);
if (from != to) {
transformed_value = CSSIdentifierValue::Create(to);
}
} else if (const auto* percentage =
DynamicTo<CSSPrimitiveValue>(value_.Get())) {
DCHECK(percentage->IsPercentage());
float from = percentage->GetFloatValue();
float to = TransformAnchorPercentage(from, logical_axis, transform);
if (from != to) {
transformed_value = CSSNumericLiteralValue::Create(
to, CSSPrimitiveValue::UnitType::kPercentage);
}
}
// The fallback can contain anchors.
const CSSPrimitiveValue* transformed_fallback = fallback_.Get();
if (const auto* math_function =
DynamicTo<CSSMathFunctionValue>(fallback_.Get())) {
transformed_fallback = math_function->TransformAnchors(
logical_axis, transform, writing_direction);
}
if (transformed_value != value_ || transformed_fallback != fallback_) {
// Either the value or the fallback was transformed.
return MakeGarbageCollected<CSSMathExpressionAnchorQuery>(
type_, anchor_specifier_, *transformed_value, transformed_fallback);
}
// No transformation.
return this;
}
void CSSMathExpressionAnchorQuery::Trace(Visitor* visitor) const {
visitor->Trace(anchor_specifier_);
visitor->Trace(value_);
visitor->Trace(fallback_);
CSSMathExpressionNode::Trace(visitor);
}
// ------ End of CSSMathExpressionAnchorQuery member functions ------
class CSSMathExpressionNodeParser {
STACK_ALLOCATED();
public:
using Flag = CSSMathExpressionNode::Flag;
using Flags = CSSMathExpressionNode::Flags;
// A struct containing parser state that varies within the expression tree.
struct State {
STACK_ALLOCATED();
public:
uint8_t depth;
bool allow_size_keyword;
static_assert(uint8_t(kMaxExpressionDepth + 1) == kMaxExpressionDepth + 1);
State() : depth(0), allow_size_keyword(false) {}
State(const State&) = default;
State& operator=(const State&) = default;
};
CSSMathExpressionNodeParser(
const CSSParserContext& context,
const Flags parsing_flags,
CSSAnchorQueryTypes allowed_anchor_queries,
const HashMap<CSSValueID, double>& color_channel_keyword_values)
: context_(context),
allowed_anchor_queries_(allowed_anchor_queries),
parsing_flags_(parsing_flags),
color_channel_keyword_values_(color_channel_keyword_values) {}
bool IsSupportedMathFunction(CSSValueID function_id) {
switch (function_id) {
case CSSValueID::kMin:
case CSSValueID::kMax:
case CSSValueID::kClamp:
case CSSValueID::kCalc:
case CSSValueID::kWebkitCalc:
case CSSValueID::kSin:
case CSSValueID::kCos:
case CSSValueID::kTan:
case CSSValueID::kAsin:
case CSSValueID::kAcos:
case CSSValueID::kAtan:
case CSSValueID::kAtan2:
return true;
case CSSValueID::kPow:
case CSSValueID::kSqrt:
case CSSValueID::kHypot:
case CSSValueID::kLog:
case CSSValueID::kExp:
return RuntimeEnabledFeatures::CSSExponentialFunctionsEnabled();
case CSSValueID::kRound:
case CSSValueID::kMod:
case CSSValueID::kRem:
return RuntimeEnabledFeatures::CSSSteppedValueFunctionsEnabled();
case CSSValueID::kAbs:
case CSSValueID::kSign:
return RuntimeEnabledFeatures::CSSSignRelatedFunctionsEnabled();
case CSSValueID::kAnchor:
case CSSValueID::kAnchorSize:
return RuntimeEnabledFeatures::CSSAnchorPositioningEnabled();
case CSSValueID::kProgress:
case CSSValueID::kMediaProgress:
return RuntimeEnabledFeatures::CSSProgressNotationEnabled();
case CSSValueID::kCalcSize:
return RuntimeEnabledFeatures::CSSCalcSizeFunctionEnabled();
// TODO(crbug.com/1284199): Support other math functions.
default:
return false;
}
}
CSSMathExpressionNode* ParseAnchorQuery(CSSValueID function_id,
CSSParserTokenRange& tokens) {
DCHECK(RuntimeEnabledFeatures::CSSAnchorPositioningEnabled());
CSSAnchorQueryType anchor_query_type;
switch (function_id) {
case CSSValueID::kAnchor:
anchor_query_type = CSSAnchorQueryType::kAnchor;
break;
case CSSValueID::kAnchorSize:
anchor_query_type = CSSAnchorQueryType::kAnchorSize;
break;
default:
return nullptr;
}
if (!(static_cast<CSSAnchorQueryTypes>(anchor_query_type) &
allowed_anchor_queries_)) {
return nullptr;
}
// |anchor_specifier| may be omitted to represent the default anchor.
const CSSValue* anchor_specifier =
css_parsing_utils::ConsumeIdent<CSSValueID::kImplicit>(tokens);
if (!anchor_specifier) {
anchor_specifier =
css_parsing_utils::ConsumeDashedIdent(tokens, context_);
}
tokens.ConsumeWhitespace();
const CSSValue* value = nullptr;
switch (anchor_query_type) {
case CSSAnchorQueryType::kAnchor:
value = css_parsing_utils::ConsumeIdent<
CSSValueID::kTop, CSSValueID::kLeft, CSSValueID::kRight,
CSSValueID::kBottom, CSSValueID::kStart, CSSValueID::kEnd,
CSSValueID::kSelfStart, CSSValueID::kSelfEnd, CSSValueID::kCenter>(
tokens);
if (!value) {
value = css_parsing_utils::ConsumePercent(
tokens, context_, CSSPrimitiveValue::ValueRange::kAll);
}
break;
case CSSAnchorQueryType::kAnchorSize:
value = css_parsing_utils::ConsumeIdent<
CSSValueID::kWidth, CSSValueID::kHeight, CSSValueID::kBlock,
CSSValueID::kInline, CSSValueID::kSelfBlock,
CSSValueID::kSelfInline>(tokens);
break;
}
if (!value) {
return nullptr;
}
const CSSPrimitiveValue* fallback = nullptr;
if (css_parsing_utils::ConsumeCommaIncludingWhitespace(tokens)) {
fallback = css_parsing_utils::ConsumeLengthOrPercent(
tokens, context_, CSSPrimitiveValue::ValueRange::kAll,
css_parsing_utils::UnitlessQuirk::kForbid, allowed_anchor_queries_);
if (!fallback) {
return nullptr;
}
}
tokens.ConsumeWhitespace();
if (!tokens.AtEnd()) {
return nullptr;
}
return MakeGarbageCollected<CSSMathExpressionAnchorQuery>(
anchor_query_type, anchor_specifier, *value, fallback);
}
bool ParseProgressNotationFromTo(
CSSParserTokenRange& tokens,
State state,
CSSMathExpressionOperation::Operands& nodes) {
if (tokens.ConsumeIncludingWhitespace().Id() != CSSValueID::kFrom) {
return false;
}
if (CSSMathExpressionNode* node = ParseValueExpression(tokens, state)) {
nodes.push_back(node);
}
if (tokens.ConsumeIncludingWhitespace().Id() != CSSValueID::kTo) {
return false;
}
if (CSSMathExpressionNode* node = ParseValueExpression(tokens, state)) {
nodes.push_back(node);
}
return true;
}
// https://drafts.csswg.org/css-values-5/#progress-func
// https://drafts.csswg.org/css-values-5/#media-progress-func
CSSMathExpressionNode* ParseProgressNotation(CSSValueID function_id,
CSSParserTokenRange& tokens,
State state) {
if (function_id != CSSValueID::kProgress &&
function_id != CSSValueID::kMediaProgress) {
return nullptr;
}
// <media-progress()> = media-progress(<media-feature> from <calc-sum> to
// <calc-sum>)
CSSMathExpressionOperation::Operands nodes;
tokens.ConsumeWhitespace();
if (function_id == CSSValueID::kMediaProgress) {
if (CSSMathExpressionKeywordLiteral* node =
ParseKeywordLiteral(tokens, CSSMathOperator::kMediaProgress)) {
nodes.push_back(node);
}
} else if (CSSMathExpressionNode* node =
ParseValueExpression(tokens, state)) {
// <progress()> = progress(<calc-sum> from <calc-sum> to <calc-sum>)
nodes.push_back(node);
}
if (!ParseProgressNotationFromTo(tokens, state, nodes)) {
return nullptr;
}
if (nodes.size() != 3u || !tokens.AtEnd() ||
!CheckProgressFunctionTypes(function_id, nodes)) {
return nullptr;
}
// Note: we don't need to resolve percents in such case,
// as all the operands are numeric literals,
// so p% / (t% - f%) will lose %.
// Note: we can not simplify media-progress.
ProgressArgsSimplificationStatus status =
CanEagerlySimplifyProgressArgs(nodes);
if (function_id == CSSValueID::kProgress &&
status != ProgressArgsSimplificationStatus::kCanNotSimplify) {
Vector<double> double_values;
double_values.reserve(nodes.size());
for (const CSSMathExpressionNode* operand : nodes) {
if (status ==
ProgressArgsSimplificationStatus::kAllArgsResolveToCanonical) {
std::optional<double> canonical_value =
operand->ComputeValueInCanonicalUnit();
CHECK(canonical_value.has_value());
double_values.push_back(canonical_value.value());
} else {
CHECK(HasDoubleValue(operand->ResolvedUnitType()));
double_values.push_back(operand->DoubleValue());
}
}
double progress_value = (double_values[0] - double_values[1]) /
(double_values[2] - double_values[1]);
return CSSMathExpressionNumericLiteral::Create(
progress_value, CSSPrimitiveValue::UnitType::kNumber);
}
return MakeGarbageCollected<CSSMathExpressionOperation>(
CalculationResultCategory::kCalcNumber, std::move(nodes),
function_id == CSSValueID::kProgress ? CSSMathOperator::kProgress
: CSSMathOperator::kMediaProgress);
}
CSSMathExpressionNode* ParseCalcSize(CSSValueID function_id,
CSSParserTokenRange& tokens,
State state) {
if (function_id != CSSValueID::kCalcSize ||
!parsing_flags_.Has(Flag::AllowCalcSize)) {
return nullptr;
}
DCHECK(RuntimeEnabledFeatures::CSSCalcSizeFunctionEnabled());
// TODO(https://crbug.com/313072): Restrict usage of calc-size() inside of
// calc(), probably along the lines of
// https://github.com/w3c/csswg-drafts/issues/626#issuecomment-1881898328
tokens.ConsumeWhitespace();
CSSMathExpressionNode* basis = nullptr;
CSSValueID id = tokens.Peek().Id();
bool basis_is_any = id == CSSValueID::kAny;
if (id != CSSValueID::kInvalid &&
(id == CSSValueID::kAny ||
(id == CSSValueID::kAuto &&
parsing_flags_.Has(Flag::AllowAutoInCalcSize)) ||
css_parsing_utils::ValidWidthOrHeightKeyword(id, context_))) {
// TODO(https://crbug.com/313072): Also allow 'auto' for some properties
// (not max-*, though, since they don't take 'auto').
// Note: We don't want to accept 'none' (for 'max-*' properties) since
// it's not meaningful for animation, since it's equivalent to infinity.
tokens.ConsumeIncludingWhitespace();
basis = CSSMathExpressionKeywordLiteral::Create(
id, CSSMathOperator::kCalcSize);
} else {
basis = ParseValueExpression(tokens, state);
if (!basis) {
return nullptr;
}
// TODO(https://crbug.com/313072): If basis is a calc-size()
// expression whose basis is 'any', set basis_is_any to true.
}
if (!css_parsing_utils::ConsumeCommaIncludingWhitespace(tokens)) {
return nullptr;
}
state.allow_size_keyword = !basis_is_any;
CSSMathExpressionNode* calculation = ParseValueExpression(tokens, state);
if (!calculation) {
return nullptr;
}
return CSSMathExpressionOperation::CreateCalcSizeOperation(basis,
calculation);
}
CSSMathExpressionNode* ParseMathFunction(CSSValueID function_id,
CSSParserTokenRange& tokens,
State state) {
if (!IsSupportedMathFunction(function_id)) {
return nullptr;
}
if (RuntimeEnabledFeatures::CSSAnchorPositioningEnabled()) {
if (auto* anchor_query = ParseAnchorQuery(function_id, tokens)) {
context_.Count(WebFeature::kCSSAnchorPositioning);
return anchor_query;
}
}
if (RuntimeEnabledFeatures::CSSProgressNotationEnabled()) {
if (CSSMathExpressionNode* progress =
ParseProgressNotation(function_id, tokens, state)) {
return progress;
}
}
if (RuntimeEnabledFeatures::CSSCalcSizeFunctionEnabled()) {
if (CSSMathExpressionNode* calc_size =
ParseCalcSize(function_id, tokens, state)) {
return calc_size;
}
}
// "arguments" refers to comma separated ones.
wtf_size_t min_argument_count = 1;
wtf_size_t max_argument_count = std::numeric_limits<wtf_size_t>::max();
switch (function_id) {
case CSSValueID::kCalc:
case CSSValueID::kWebkitCalc:
max_argument_count = 1;
break;
case CSSValueID::kMin:
case CSSValueID::kMax:
break;
case CSSValueID::kClamp:
min_argument_count = 3;
max_argument_count = 3;
break;
case CSSValueID::kSin:
case CSSValueID::kCos:
case CSSValueID::kTan:
case CSSValueID::kAsin:
case CSSValueID::kAcos:
case CSSValueID::kAtan:
max_argument_count = 1;
break;
case CSSValueID::kPow:
DCHECK(RuntimeEnabledFeatures::CSSExponentialFunctionsEnabled());
max_argument_count = 2;
min_argument_count = 2;
break;
case CSSValueID::kExp:
case CSSValueID::kSqrt:
DCHECK(RuntimeEnabledFeatures::CSSExponentialFunctionsEnabled());
max_argument_count = 1;
break;
case CSSValueID::kHypot:
DCHECK(RuntimeEnabledFeatures::CSSExponentialFunctionsEnabled());
max_argument_count = kMaxExpressionDepth;
break;
case CSSValueID::kLog:
DCHECK(RuntimeEnabledFeatures::CSSExponentialFunctionsEnabled());
max_argument_count = 2;
break;
case CSSValueID::kRound:
DCHECK(RuntimeEnabledFeatures::CSSSteppedValueFunctionsEnabled());
max_argument_count = 3;
min_argument_count = 2;
break;
case CSSValueID::kMod:
case CSSValueID::kRem:
DCHECK(RuntimeEnabledFeatures::CSSSteppedValueFunctionsEnabled());
max_argument_count = 2;
min_argument_count = 2;
break;
case CSSValueID::kAtan2:
max_argument_count = 2;
min_argument_count = 2;
break;
case CSSValueID::kAbs:
case CSSValueID::kSign:
DCHECK(RuntimeEnabledFeatures::CSSSignRelatedFunctionsEnabled());
max_argument_count = 1;
min_argument_count = 1;
break;
// TODO(crbug.com/1284199): Support other math functions.
default:
break;
}
HeapVector<Member<const CSSMathExpressionNode>> nodes;
// Parse the initial (optional) <rounding-strategy> argument to the round()
// function.
if (function_id == CSSValueID::kRound) {
CSSMathExpressionNode* rounding_strategy = ParseRoundingStrategy(tokens);
if (rounding_strategy) {
nodes.push_back(rounding_strategy);
}
}
while (!tokens.AtEnd() && nodes.size() < max_argument_count) {
if (nodes.size()) {
if (!css_parsing_utils::ConsumeCommaIncludingWhitespace(tokens)) {
return nullptr;
}
}
tokens.ConsumeWhitespace();
CSSMathExpressionNode* node = ParseValueExpression(tokens, state);
if (!node) {
return nullptr;
}
nodes.push_back(node);
}
if (!tokens.AtEnd() || nodes.size() < min_argument_count) {
return nullptr;
}
switch (function_id) {
case CSSValueID::kCalc:
case CSSValueID::kWebkitCalc: {
const CSSMathExpressionNode* node = nodes.front();
if (node->Category() == kCalcIntrinsicSize) {
return nullptr;
}
return const_cast<CSSMathExpressionNode*>(node);
}
case CSSValueID::kMin:
case CSSValueID::kMax:
case CSSValueID::kClamp: {
CSSMathOperator op = CSSMathOperator::kMin;
if (function_id == CSSValueID::kMax) {
op = CSSMathOperator::kMax;
}
if (function_id == CSSValueID::kClamp) {
op = CSSMathOperator::kClamp;
}
CSSMathExpressionNode* node =
CSSMathExpressionOperation::CreateComparisonFunctionSimplified(
std::move(nodes), op);
if (node) {
context_.Count(WebFeature::kCSSComparisonFunctions);
}
return node;
}
case CSSValueID::kSin:
case CSSValueID::kCos:
case CSSValueID::kTan:
case CSSValueID::kAsin:
case CSSValueID::kAcos:
case CSSValueID::kAtan:
case CSSValueID::kAtan2:
return CSSMathExpressionOperation::
CreateTrigonometricFunctionSimplified(std::move(nodes),
function_id);
case CSSValueID::kPow:
case CSSValueID::kSqrt:
case CSSValueID::kHypot:
case CSSValueID::kLog:
case CSSValueID::kExp:
DCHECK(RuntimeEnabledFeatures::CSSExponentialFunctionsEnabled());
return CSSMathExpressionOperation::CreateExponentialFunction(
std::move(nodes), function_id);
case CSSValueID::kRound:
case CSSValueID::kMod:
case CSSValueID::kRem: {
DCHECK(RuntimeEnabledFeatures::CSSSteppedValueFunctionsEnabled());
DCHECK_GE(nodes.size(), 2u);
DCHECK_LE(nodes.size(), 3u);
CSSMathOperator op;
if (function_id == CSSValueID::kRound) {
// If the first argument is a rounding strategy, use the specified
// operation and drop the argument from the list of operands.
const auto* maybe_rounding_strategy =
DynamicTo<CSSMathExpressionOperation>(*nodes[0]);
if (maybe_rounding_strategy &&
maybe_rounding_strategy->IsRoundingStrategyKeyword()) {
op = maybe_rounding_strategy->OperatorType();
nodes.EraseAt(0);
} else {
op = CSSMathOperator::kRoundNearest;
}
if (nodes.size() != 2) {
return nullptr;
}
} else if (function_id == CSSValueID::kMod) {
op = CSSMathOperator::kMod;
} else {
op = CSSMathOperator::kRem;
}
return CSSMathExpressionOperation::CreateSteppedValueFunction(
std::move(nodes), op);
}
case CSSValueID::kAbs:
case CSSValueID::kSign:
// TODO(seokho): Relative and Percent values cannot be evaluated at the
// parsing time. So we should implement cannot be simplified value
// using CalculationExpressionNode
DCHECK(RuntimeEnabledFeatures::CSSSignRelatedFunctionsEnabled());
return CSSMathExpressionOperation::CreateSignRelatedFunction(
std::move(nodes), function_id);
// TODO(crbug.com/1284199): Support other math functions.
default:
return nullptr;
}
}
private:
CSSMathExpressionNode* ParseValue(CSSParserTokenRange& tokens, State state) {
CSSParserToken token = tokens.ConsumeIncludingWhitespace();
if (token.Id() == CSSValueID::kInfinity) {
return CSSMathExpressionNumericLiteral::Create(
std::numeric_limits<double>::infinity(),
CSSPrimitiveValue::UnitType::kNumber);
}
if (token.Id() == CSSValueID::kNegativeInfinity) {
return CSSMathExpressionNumericLiteral::Create(
-std::numeric_limits<double>::infinity(),
CSSPrimitiveValue::UnitType::kNumber);
}
if (token.Id() == CSSValueID::kNan) {
return CSSMathExpressionNumericLiteral::Create(
std::numeric_limits<double>::quiet_NaN(),
CSSPrimitiveValue::UnitType::kNumber);
}
if (token.Id() == CSSValueID::kPi) {
return CSSMathExpressionNumericLiteral::Create(
M_PI, CSSPrimitiveValue::UnitType::kNumber);
}
if (token.Id() == CSSValueID::kE) {
return CSSMathExpressionNumericLiteral::Create(
M_E, CSSPrimitiveValue::UnitType::kNumber);
}
if (state.allow_size_keyword && token.Id() == CSSValueID::kSize) {
return CSSMathExpressionKeywordLiteral::Create(
CSSValueID::kSize, CSSMathOperator::kCalcSize);
}
if (!(token.GetType() == kNumberToken ||
(token.GetType() == kPercentageToken &&
parsing_flags_.Has(Flag::AllowPercent)) ||
token.GetType() == kDimensionToken)) {
// For relative color syntax. Swap in the associated value of a color
// channel here. e.g. color(from color(srgb 1 0 0) calc(r * 2) 0 0) should
// swap in "1" for the value of "r" in the calc expression.
if (color_channel_keyword_values_.Contains(token.Id())) {
return CSSMathExpressionNumericLiteral::Create(
color_channel_keyword_values_.at(token.Id()),
CSSPrimitiveValue::UnitType::kNumber);
}
return nullptr;
}
CSSPrimitiveValue::UnitType type = token.GetUnitType();
if (UnitCategory(type) == kCalcOther) {
return nullptr;
}
return CSSMathExpressionNumericLiteral::Create(
CSSNumericLiteralValue::Create(token.NumericValue(), type));
}
CSSMathExpressionNode* ParseRoundingStrategy(CSSParserTokenRange& tokens) {
CSSMathOperator rounding_op = CSSMathOperator::kInvalid;
switch (tokens.Peek().Id()) {
case CSSValueID::kNearest:
rounding_op = CSSMathOperator::kRoundNearest;
break;
case CSSValueID::kUp:
rounding_op = CSSMathOperator::kRoundUp;
break;
case CSSValueID::kDown:
rounding_op = CSSMathOperator::kRoundDown;
break;
case CSSValueID::kToZero:
rounding_op = CSSMathOperator::kRoundToZero;
break;
default:
return nullptr;
}
tokens.ConsumeIncludingWhitespace();
return MakeGarbageCollected<CSSMathExpressionOperation>(
CalculationResultCategory::kCalcNumber, rounding_op);
}
CSSMathExpressionNode* ParseValueTerm(CSSParserTokenRange& tokens,
State state) {
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, state);
if (!result || !inner_range.AtEnd()) {
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();
return ParseMathFunction(function_id, inner_range, state);
}
return ParseValue(tokens, state);
}
CSSMathExpressionNode* ParseValueMultiplicativeExpression(
CSSParserTokenRange& tokens,
State state) {
if (tokens.AtEnd()) {
return nullptr;
}
CSSMathExpressionNode* result = ParseValueTerm(tokens, state);
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, state);
if (!rhs) {
return nullptr;
}
result = CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
result, rhs, math_operator);
if (!result) {
return nullptr;
}
}
return result;
}
CSSMathExpressionNode* ParseAdditiveValueExpression(
CSSParserTokenRange& tokens,
State state) {
if (tokens.AtEnd()) {
return nullptr;
}
CSSMathExpressionNode* result =
ParseValueMultiplicativeExpression(tokens, state);
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, state);
if (!rhs) {
return nullptr;
}
result = CSSMathExpressionOperation::CreateArithmeticOperationSimplified(
result, rhs, math_operator);
if (!result) {
return nullptr;
}
}
if (auto* operation = DynamicTo<CSSMathExpressionOperation>(result)) {
if (operation->IsAddOrSubtract()) {
result = MaybeSimplifySumNode(operation);
}
}
return result;
}
CSSMathExpressionKeywordLiteral* ParseKeywordLiteral(
CSSParserTokenRange& tokens,
CSSMathOperator op) {
const CSSParserToken& token = tokens.ConsumeIncludingWhitespace();
if (token.GetType() == kIdentToken) {
return CSSMathExpressionKeywordLiteral::Create(token.Id(), op);
}
return nullptr;
}
CSSMathExpressionNode* ParseValueExpression(CSSParserTokenRange& tokens,
State state) {
if (++state.depth > kMaxExpressionDepth) {
return nullptr;
}
return ParseAdditiveValueExpression(tokens, state);
}
const CSSParserContext& context_;
const CSSAnchorQueryTypes allowed_anchor_queries_;
const Flags parsing_flags_;
const HashMap<CSSValueID, double>& color_channel_keyword_values_;
};
scoped_refptr<const CalculationValue> CSSMathExpressionNode::ToCalcValue(
const CSSLengthResolver& length_resolver,
Length::ValueRange range,
bool allows_negative_percentage_reference) const {
if (auto maybe_pixels_and_percent = ToPixelsAndPercent(length_resolver)) {
// 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 (IsNaN(*maybe_pixels_and_percent,
allows_negative_percentage_reference)) {
maybe_pixels_and_percent = CreateClampedSamePixelsAndPercent(
std::numeric_limits<float>::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(length_resolver);
std::optional<PixelsAndPercent> 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;
if (!value.has_explicit_pixels) {
CHECK(!pixels);
return CSSMathExpressionNumericLiteral::Create(
percent, CSSPrimitiveValue::UnitType::kPercentage);
}
if (!value.has_explicit_percent) {
CHECK(!percent);
return CSSMathExpressionNumericLiteral::Create(
pixels, CSSPrimitiveValue::UnitType::kPixels);
}
CSSMathOperator op = CSSMathOperator::kAdd;
if (pixels < 0) {
pixels = -pixels;
op = CSSMathOperator::kSubtract;
}
return CSSMathExpressionOperation::CreateArithmeticOperation(
CSSMathExpressionNumericLiteral::Create(CSSNumericLiteralValue::Create(
percent, CSSPrimitiveValue::UnitType::kPercentage)),
CSSMathExpressionNumericLiteral::Create(CSSNumericLiteralValue::Create(
pixels, CSSPrimitiveValue::UnitType::kPixels)),
op);
}
// static
CSSMathExpressionNode* CSSMathExpressionNode::Create(
const CalculationExpressionNode& node) {
if (node.IsPixelsAndPercent()) {
const auto& pixels_and_percent =
To<CalculationExpressionPixelsAndPercentNode>(node);
return Create(pixels_and_percent.GetPixelsAndPercent());
}
if (node.IsIdentifier()) {
return CSSMathExpressionIdentifierLiteral::Create(
To<CalculationExpressionIdentifierNode>(node).Value());
}
if (node.IsSizingKeyword()) {
return CSSMathExpressionKeywordLiteral::Create(
SizingKeywordToCSSValueID(
To<CalculationExpressionSizingKeywordNode>(node).Value()),
CSSMathOperator::kCalcSize);
}
if (node.IsNumber()) {
return CSSMathExpressionNumericLiteral::Create(
To<CalculationExpressionNumberNode>(node).Value(),
CSSPrimitiveValue::UnitType::kNumber);
}
DCHECK(node.IsOperation());
const auto& operation = To<CalculationExpressionOperationNode>(node);
const auto& children = operation.GetChildren();
const auto calc_op = operation.GetOperator();
switch (calc_op) {
case CalculationOperator::kMultiply: {
DCHECK_EQ(children.size(), 2u);
return CSSMathExpressionOperation::CreateArithmeticOperation(
Create(*children.front()), Create(*children.back()),
CSSMathOperator::kMultiply);
}
case CalculationOperator::kAdd:
case CalculationOperator::kSubtract: {
DCHECK_EQ(children.size(), 2u);
auto* lhs = Create(*children[0]);
auto* rhs = Create(*children[1]);
CSSMathOperator op = (calc_op == CalculationOperator::kAdd)
? CSSMathOperator::kAdd
: CSSMathOperator::kSubtract;
return CSSMathExpressionOperation::CreateArithmeticOperation(lhs, rhs,
op);
}
case CalculationOperator::kMin:
case CalculationOperator::kMax: {
DCHECK(children.size());
CSSMathExpressionOperation::Operands operands;
for (const auto& child : children) {
operands.push_back(Create(*child));
}
CSSMathOperator op = (calc_op == CalculationOperator::kMin)
? CSSMathOperator::kMin
: CSSMathOperator::kMax;
return CSSMathExpressionOperation::CreateComparisonFunction(
std::move(operands), op);
}
case CalculationOperator::kClamp: {
DCHECK_EQ(children.size(), 3u);
CSSMathExpressionOperation::Operands operands;
for (const auto& child : children) {
operands.push_back(Create(*child));
}
return CSSMathExpressionOperation::CreateComparisonFunction(
std::move(operands), CSSMathOperator::kClamp);
}
case CalculationOperator::kRoundNearest:
case CalculationOperator::kRoundUp:
case CalculationOperator::kRoundDown:
case CalculationOperator::kRoundToZero:
case CalculationOperator::kMod:
case CalculationOperator::kRem: {
DCHECK_EQ(children.size(), 2u);
CSSMathExpressionOperation::Operands operands;
for (const auto& child : children) {
operands.push_back(Create(*child));
}
CSSMathOperator op;
if (calc_op == CalculationOperator::kRoundNearest) {
op = CSSMathOperator::kRoundNearest;
} else if (calc_op == CalculationOperator::kRoundUp) {
op = CSSMathOperator::kRoundUp;
} else if (calc_op == CalculationOperator::kRoundDown) {
op = CSSMathOperator::kRoundDown;
} else if (calc_op == CalculationOperator::kRoundToZero) {
op = CSSMathOperator::kRoundToZero;
} else if (calc_op == CalculationOperator::kMod) {
op = CSSMathOperator::kMod;
} else {
op = CSSMathOperator::kRem;
}
return CSSMathExpressionOperation::CreateSteppedValueFunction(
std::move(operands), op);
}
case CalculationOperator::kHypot: {
DCHECK_GE(children.size(), 1u);
CSSMathExpressionOperation::Operands operands;
for (const auto& child : children) {
operands.push_back(Create(*child));
}
return CSSMathExpressionOperation::CreateExponentialFunction(
std::move(operands), CSSValueID::kHypot);
}
case CalculationOperator::kAbs:
case CalculationOperator::kSign: {
DCHECK_EQ(children.size(), 1u);
CSSMathExpressionOperation::Operands operands;
operands.push_back(Create(*children.front()));
CSSValueID op = calc_op == CalculationOperator::kAbs ? CSSValueID::kAbs
: CSSValueID::kSign;
return CSSMathExpressionOperation::CreateSignRelatedFunction(
std::move(operands), op);
}
case CalculationOperator::kProgress:
case CalculationOperator::kMediaProgress: {
CHECK_EQ(children.size(), 3u);
CSSMathExpressionOperation::Operands operands;
operands.push_back(Create(*children.front()));
operands.push_back(Create(*children[1]));
operands.push_back(Create(*children.back()));
CSSMathOperator op = calc_op == CalculationOperator::kProgress
? CSSMathOperator::kProgress
: CSSMathOperator::kMediaProgress;
return MakeGarbageCollected<CSSMathExpressionOperation>(
CalculationResultCategory::kCalcNumber, std::move(operands), op);
}
case CalculationOperator::kCalcSize: {
CHECK_EQ(children.size(), 2u);
return CSSMathExpressionOperation::CreateCalcSizeOperation(
Create(*children.front()), Create(*children.back()));
}
case CalculationOperator::kInvalid:
NOTREACHED();
return nullptr;
}
}
// static
CSSMathExpressionNode* CSSMathExpressionNode::ParseMathFunction(
CSSValueID function_id,
CSSParserTokenRange tokens,
const CSSParserContext& context,
const Flags parsing_flags,
CSSAnchorQueryTypes allowed_anchor_queries,
const HashMap<CSSValueID, double>& color_channel_keyword_values) {
CSSMathExpressionNodeParser parser(context, parsing_flags,
allowed_anchor_queries,
color_channel_keyword_values);
CSSMathExpressionNodeParser::State state;
CSSMathExpressionNode* result =
parser.ParseMathFunction(function_id, tokens, state);
// TODO(pjh0718): Do simplificiation for result above.
return result;
}
} // namespace blink
WTF_ALLOW_CLEAR_UNUSED_SLOTS_WITH_MEM_FUNCTIONS(
blink::CSSMathExpressionNodeWithOperator)