/* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2021, Tobias Christiansen * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2022-2023, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ #include "CalculatedStyleValue.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::CSS { static Optional add_the_types(Vector> const& nodes) { Optional left_type; for (auto const& value : nodes) { auto right_type = value->numeric_type(); if (!right_type.has_value()) return {}; if (left_type.has_value()) { left_type = left_type->added_to(right_type.value()); } else { left_type = right_type; } if (!left_type.has_value()) return {}; } return left_type; } static Optional add_the_types(CalculationNode const& a, CalculationNode const& b) { auto a_type = a.numeric_type(); auto b_type = b.numeric_type(); if (!a_type.has_value() || !b_type.has_value()) return {}; return a_type->added_to(*b_type); } static Optional add_the_types(CalculationNode const& a, CalculationNode const& b, CalculationNode const& c) { auto a_type = a.numeric_type(); auto b_type = b.numeric_type(); auto c_type = c.numeric_type(); if (!a_type.has_value() || !b_type.has_value() || !c_type.has_value()) return {}; auto a_and_b_type = a_type->added_to(*b_type); if (!a_and_b_type.has_value()) return {}; return a_and_b_type->added_to(*c_type); } static Optional multiply_the_types(Vector> const& nodes) { // At a * sub-expression, multiply the types of the left and right arguments. // The sub-expression’s type is the returned result. Optional left_type; for (auto const& value : nodes) { auto right_type = value->numeric_type(); if (!right_type.has_value()) return {}; if (left_type.has_value()) { left_type = left_type->multiplied_by(right_type.value()); } else { left_type = right_type; } if (!left_type.has_value()) return {}; } return left_type; } template static NonnullRefPtr simplify_children_vector(T const& original, CalculationContext const& context, CalculationResolutionContext const& resolution_context) { Vector> simplified_children; simplified_children.ensure_capacity(original.children().size()); bool any_changed = false; for (auto const& child : original.children()) { auto simplified = simplify_a_calculation_tree(child, context, resolution_context); if (simplified != child) any_changed = true; simplified_children.append(move(simplified)); } if (any_changed) return T::create(move(simplified_children)); return original; } template static NonnullRefPtr simplify_child(T const& original, NonnullRefPtr const& child, CalculationContext const& context, CalculationResolutionContext const& resolution_context) { auto simplified = simplify_a_calculation_tree(child, context, resolution_context); if (simplified != child) return T::create(move(simplified)); return original; } template static NonnullRefPtr simplify_2_children(T const& original, NonnullRefPtr const& child_1, NonnullRefPtr const& child_2, CalculationContext const& context, CalculationResolutionContext const& resolution_context) { auto simplified_1 = simplify_a_calculation_tree(child_1, context, resolution_context); auto simplified_2 = simplify_a_calculation_tree(child_2, context, resolution_context); if (simplified_1 != child_1 || simplified_2 != child_2) return T::create(move(simplified_1), move(simplified_2)); return original; } static String serialize_a_calculation_tree(CalculationNode const&, CalculationContext const&, CSSStyleValue::SerializationMode); // https://drafts.csswg.org/css-values-4/#serialize-a-math-function static String serialize_a_math_function(CalculationNode const& fn, CalculationContext const& context, CSSStyleValue::SerializationMode serialization_mode) { // To serialize a math function fn: // 1. If the root of the calculation tree fn represents is a numeric value (number, percentage, or dimension), and // the serialization being produced is of a computed value or later, then clamp the value to the range allowed // for its context (if necessary), then serialize the value as normal and return the result. if (fn.type() == CalculationNode::Type::Numeric && serialization_mode == CSSStyleValue::SerializationMode::ResolvedValue) { // FIXME: Clamp the value. Note that we might have an infinite/nan value here. return static_cast(fn).value_to_string(); } // 2. If fn represents an infinite or NaN value: if (fn.type() == CalculationNode::Type::Numeric) { auto const& numeric_node = static_cast(fn); if (auto infinite_or_nan = numeric_node.infinite_or_nan_value(); infinite_or_nan.has_value()) { // 1. Let s be the string "calc(". StringBuilder builder; builder.append("calc("sv); // 2. Serialize the keyword infinity, -infinity, or NaN, as appropriate to represent the value, and append it to s. switch (infinite_or_nan.value()) { case NonFiniteValue::Infinity: builder.append("infinity"sv); break; case NonFiniteValue::NegativeInfinity: builder.append("-infinity"sv); break; case NonFiniteValue::NaN: builder.append("NaN"sv); break; default: VERIFY_NOT_REACHED(); } // 3. If fn’s type is anything other than «[ ]» (empty, representing a ), append " * " to s. // Create a numeric value in the canonical unit for fn’s type (such as px for ), with a value of 1. // Serialize this numeric value and append it to s. if (!numeric_node.value().has()) { numeric_node.value().visit( [&builder](Angle const&) { builder.append(" * 1deg"sv); }, [&builder](Flex const&) { builder.append(" * 1fr"sv); }, [&builder](Frequency const&) { builder.append(" * 1hz"sv); }, [&builder](Length const&) { builder.append(" * 1px"sv); }, [](Number const&) { VERIFY_NOT_REACHED(); }, [&builder](Percentage const&) { builder.append(" * 1%"sv); }, [&builder](Resolution const&) { builder.append(" * 1dppx"sv); }, [&builder](Time const&) { builder.append(" * 1s"sv); }); } // 4. Append ")" to s, then return it. builder.append(')'); return builder.to_string_without_validation(); } } // 3. If the calculation tree’s root node is a numeric value, or a calc-operator node, let s be a string initially // containing "calc(". // Otherwise, let s be a string initially containing the name of the root node, lowercased (such as "sin" or // "max"), followed by a "(" (open parenthesis). StringBuilder builder; if (fn.type() == CalculationNode::Type::Numeric || fn.is_calc_operator_node()) { builder.append("calc("sv); } else { builder.appendff("{}(", fn.name()); } // 4. For each child of the root node, serialize the calculation tree. // If a result of this serialization starts with a "(" (open parenthesis) and ends with a ")" (close parenthesis), // remove those characters from the result. // Concatenate all of the results using ", " (comma followed by space), then append the result to s. auto serialized_tree_without_parentheses = [&](CalculationNode const& tree) { auto tree_serialized = serialize_a_calculation_tree(tree, context, serialization_mode); if (tree_serialized.starts_with('(') && tree_serialized.ends_with(')')) { tree_serialized = MUST(tree_serialized.substring_from_byte_offset_with_shared_superstring(1, tree_serialized.byte_count() - 2)); } return tree_serialized; }; // Spec issue: https://github.com/w3c/csswg-drafts/issues/11783 // The three AD-HOCs in this step are mentioned there. // AD-HOC: Numeric nodes have no children and should serialize directly. // AD-HOC: calc-operator nodes should also serialize directly, instead of separating their children by commas.# if (fn.type() == CalculationNode::Type::Numeric || fn.is_calc_operator_node()) { builder.append(serialized_tree_without_parentheses(fn)); } else { Vector serialized_children; // AD-HOC: For `clamp()`, the first child is a , which is incompatible with "serialize a calculation tree". // So, we serialize it directly first, and hope for the best. if (fn.type() == CalculationNode::Type::Round) { auto rounding_strategy = static_cast(fn).rounding_strategy(); serialized_children.append(MUST(String::from_utf8(CSS::to_string(rounding_strategy)))); } for (auto const& child : fn.children()) { serialized_children.append(serialized_tree_without_parentheses(child)); } builder.join(", "sv, serialized_children); } // 5. Append ")" (close parenthesis) to s. builder.append(')'); // 6. Return s. return builder.to_string_without_validation(); } // https://drafts.csswg.org/css-values-4/#sort-a-calculations-children static Vector> sort_a_calculations_children(Vector> nodes) { // 1. Let ret be an empty list. Vector> ret; // 2. If nodes contains a number, remove it from nodes and append it to ret. auto index_of_number = nodes.find_first_index_if([](NonnullRefPtr const& node) { if (node->type() != CalculationNode::Type::Numeric) return false; return static_cast(*node).value().has(); }); if (index_of_number.has_value()) { ret.append(nodes.take(*index_of_number)); } // 3. If nodes contains a percentage, remove it from nodes and append it to ret. auto index_of_percentage = nodes.find_first_index_if([](NonnullRefPtr const& node) { if (node->type() != CalculationNode::Type::Numeric) return false; return static_cast(*node).value().has(); }); if (index_of_percentage.has_value()) { ret.append(nodes.take(*index_of_percentage)); } // 4. If nodes contains any dimensions, remove them from nodes, sort them by their units, ordered ASCII // case-insensitively, and append them to ret. Vector> dimensions; dimensions.ensure_capacity(nodes.size()); auto next_dimension_index = [&nodes]() { return nodes.find_first_index_if([](NonnullRefPtr const& node) { if (node->type() != CalculationNode::Type::Numeric) return false; return static_cast(*node).value().visit( [](Number const&) { return false; }, [](Percentage const&) { return false; }, [](auto const&) { return true; }); }); }; for (auto index_of_dimension = next_dimension_index(); index_of_dimension.has_value(); index_of_dimension = next_dimension_index()) { dimensions.append(nodes.take(*index_of_dimension)); } quick_sort(dimensions, [](NonnullRefPtr const& a, NonnullRefPtr const& b) { auto get_unit = [](NonnullRefPtr const& node) -> StringView { auto const& numeric_node = static_cast(*node); return numeric_node.value().visit( [](Number const&) -> StringView { VERIFY_NOT_REACHED(); }, [](Percentage const&) -> StringView { VERIFY_NOT_REACHED(); }, [](auto const& dimension) -> StringView { return dimension.unit_name(); }); }; auto a_unit = get_unit(a); auto b_unit = get_unit(b); // NOTE: Our unit name strings are always lowercase, so we don't have to do anything special for a case-insensitive match. return a_unit < b_unit; }); ret.extend(dimensions); // 5. If nodes still contains any items, append them to ret in the same order. if (!nodes.is_empty()) ret.extend(nodes); // 6. Return ret. return ret; } // https://drafts.csswg.org/css-values-4/#serialize-a-calculation-tree static String serialize_a_calculation_tree(CalculationNode const& root, CalculationContext const& context, CSSStyleValue::SerializationMode serialization_mode) { // 1. Let root be the root node of the calculation tree. // NOTE: Already the case. // 2. If root is a numeric value, or a non-math function, serialize root per the normal rules for it and return the result. // FIXME: Support non-math functions in calculation trees. if (root.type() == CalculationNode::Type::Numeric) return static_cast(root).value_to_string(); // 3. If root is anything but a Sum, Negate, Product, or Invert node, serialize a math function for the function // corresponding to the node type, treating the node’s children as the function’s comma-separated calculation // arguments, and return the result. if (!first_is_one_of(root.type(), CalculationNode::Type::Sum, CalculationNode::Type::Product, CalculationNode::Type::Negate, CalculationNode::Type::Invert)) { return serialize_a_math_function(root, context, serialization_mode); } // 4. If root is a Negate node, let s be a string initially containing "(-1 * ". if (root.type() == CalculationNode::Type::Negate) { StringBuilder builder; builder.append("(-1 * "sv); // Serialize root’s child, and append it to s. builder.append(serialize_a_calculation_tree(root.children().first(), context, serialization_mode)); // Append ")" to s, then return it. builder.append(')'); return builder.to_string_without_validation(); } // 5. If root is an Invert node, let s be a string initially containing "(1 / ". if (root.type() == CalculationNode::Type::Invert) { StringBuilder builder; builder.append("(1 / "sv); // Serialize root’s child, and append it to s. builder.append(serialize_a_calculation_tree(root.children().first(), context, serialization_mode)); // Append ")" to s, then return it. builder.append(')'); return builder.to_string_without_validation(); } // 6. If root is a Sum node, let s be a string initially containing "(". if (root.type() == CalculationNode::Type::Sum) { StringBuilder builder; builder.append('('); auto sorted_children = sort_a_calculations_children(root.children()); // Serialize root’s first child, and append it to s. builder.append(serialize_a_calculation_tree(sorted_children.first(), context, serialization_mode)); // For each child of root beyond the first: for (auto i = 1u; i < sorted_children.size(); ++i) { auto& child = *sorted_children[i]; // 1. If child is a Negate node, append " - " to s, then serialize the Negate’s child and append the // result to s. if (child.type() == CalculationNode::Type::Negate) { builder.append(" - "sv); builder.append(serialize_a_calculation_tree(static_cast(child).child(), context, serialization_mode)); } // 2. If child is a negative numeric value, append " - " to s, then serialize the negation of child as // normal and append the result to s. else if (child.type() == CalculationNode::Type::Numeric && static_cast(child).is_negative()) { auto const& numeric_node = static_cast(child); builder.append(" - "sv); builder.append(serialize_a_calculation_tree(numeric_node.negated(context), context, serialization_mode)); } // 3. Otherwise, append " + " to s, then serialize child and append the result to s. else { builder.append(" + "sv); builder.append(serialize_a_calculation_tree(child, context, serialization_mode)); } } // Finally, append ")" to s and return it. builder.append(')'); return builder.to_string_without_validation(); } // 7. If root is a Product node, let s be a string initially containing "(". if (root.type() == CalculationNode::Type::Product) { StringBuilder builder; builder.append('('); auto sorted_children = sort_a_calculations_children(root.children()); // Serialize root’s first child, and append it to s. builder.append(serialize_a_calculation_tree(sorted_children.first(), context, serialization_mode)); // For each child of root beyond the first: for (auto i = 1u; i < sorted_children.size(); ++i) { auto& child = *sorted_children[i]; // 1. If child is an Invert node, append " / " to s, then serialize the Invert’s child and append the result to s. if (child.type() == CalculationNode::Type::Invert) { builder.append(" / "sv); builder.append(serialize_a_calculation_tree(static_cast(child).child(), context, serialization_mode)); } // 2. Otherwise, append " * " to s, then serialize child and append the result to s. else { builder.append(" * "sv); builder.append(serialize_a_calculation_tree(child, context, serialization_mode)); } } // Finally, append ")" to s and return it. builder.append(')'); return builder.to_string_without_validation(); } VERIFY_NOT_REACHED(); } CalculationNode::CalculationNode(Type type, Optional numeric_type) : m_type(type) , m_numeric_type(move(numeric_type)) { } CalculationNode::~CalculationNode() = default; StringView CalculationNode::name() const { switch (m_type) { case Type::Min: return "min"sv; case Type::Max: return "max"sv; case Type::Clamp: return "clamp"sv; case Type::Abs: return "abs"sv; case Type::Sign: return "sign"sv; case Type::Sin: return "sin"sv; case Type::Cos: return "cos"sv; case Type::Tan: return "tan"sv; case Type::Asin: return "asin"sv; case Type::Acos: return "acos"sv; case Type::Atan: return "atan"sv; case Type::Atan2: return "atan2"sv; case Type::Pow: return "pow"sv; case Type::Sqrt: return "sqrt"sv; case Type::Hypot: return "hypot"sv; case Type::Log: return "log"sv; case Type::Exp: return "exp"sv; case Type::Round: return "round"sv; case Type::Mod: return "mod"sv; case Type::Rem: return "rem"sv; case Type::Numeric: case Type::Sum: case Type::Product: case Type::Negate: case Type::Invert: return "calc"sv; } VERIFY_NOT_REACHED(); } static CSSNumericType numeric_type_from_calculated_style_value(CalculatedStyleValue::CalculationResult::Value const& value, CalculationContext const& context) { // https://drafts.csswg.org/css-values-4/#determine-the-type-of-a-calculation // Anything else is a terminal value, whose type is determined based on its CSS type. // (Unless otherwise specified, the type’s associated percent hint is null.) return value.visit( [](Number const&) { // -> // -> // the type is «[ ]» (empty map) return CSSNumericType {}; }, [](Length const&) { // -> // the type is «[ "length" → 1 ]» return CSSNumericType { CSSNumericType::BaseType::Length, 1 }; }, [](Angle const&) { // -> // the type is «[ "angle" → 1 ]» return CSSNumericType { CSSNumericType::BaseType::Angle, 1 }; }, [](Time const&) { // ->