/* * Copyright (c) 2025, Sam Atkins * Copyright (c) 2026, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSNumericValue); static Bindings::CSSNumericBaseType to_om_numeric_base_type(NumericType::BaseType source) { switch (source) { case NumericType::BaseType::Length: return Bindings::CSSNumericBaseType::Length; case NumericType::BaseType::Angle: return Bindings::CSSNumericBaseType::Angle; case NumericType::BaseType::Time: return Bindings::CSSNumericBaseType::Time; case NumericType::BaseType::Frequency: return Bindings::CSSNumericBaseType::Frequency; case NumericType::BaseType::Resolution: return Bindings::CSSNumericBaseType::Resolution; case NumericType::BaseType::Flex: return Bindings::CSSNumericBaseType::Flex; case NumericType::BaseType::Percent: return Bindings::CSSNumericBaseType::Percent; case NumericType::BaseType::__Count: VERIFY_NOT_REACHED(); } VERIFY_NOT_REACHED(); } CSSNumericValue::CSSNumericValue(JS::Realm& realm, NumericType type) : CSSStyleValue(realm) , m_type(move(type)) { } void CSSNumericValue::initialize(JS::Realm& realm) { WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSNumericValue); Base::initialize(realm); } static bool all_values_are_css_unit_values_with_the_same_unit(GC::RootVector> const& values) { VERIFY(!values.is_empty()); return all_of(values, [&](auto& value) { if (auto* unit_value = as_if(*value)) return unit_value->unit() == as(*values[0]).unit(); return false; }); } template static GC::Ref apply_math_operation_on_css_unit_values(JS::Realm& realm, GC::RootVector> const& values, Operation&& operation) { auto& first_unit_value = as(*values[0]); auto& unit = first_unit_value.unit(); double result = first_unit_value.value(); for (size_t i = 1; i < values.size(); ++i) result = operation(result, as(*values[i]).value()); return CSSUnitValue::create(realm, result, unit); } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-add WebIDL::ExceptionOr> CSSNumericValue::add(Vector const& initial_values) { auto& realm = this->realm(); // 1. Replace each item of values with the result of rectifying a numberish value for the item. // 2. If this is a CSSMathSum object, prepend the items in this’s values internal slot to values. // Otherwise, prepend this to values. // NB: We reorder the steps a little to avoid the awkward prepending. GC::RootVector> values { heap() }; if (auto const* math_sum = as_if(*this)) values.extend(math_sum->values()->values()); else values.append(*this); for (auto const& value : initial_values) values.append(rectify_a_numberish_value(realm, value)); // 3. If all of the items in values are CSSUnitValues and have the same unit, return a new CSSUnitValue whose unit // internal slot is set to that unit, and value internal slot is set to the sum of the value internal slots of // the items in values. This addition must be done "left to right" - if values is « 1, 2, 3, 4 », the result must // be (((1 + 2) + 3) + 4). (This detail is necessary to ensure interoperability in the presence of floating-point // arithmetic.) if (all_values_are_css_unit_values_with_the_same_unit(values)) return apply_math_operation_on_css_unit_values(realm, values, [](double a, double b) { return a + b; }); // 4. Let type be the result of adding the types of every item in values. If type is failure, throw a TypeError. // 5. Return a new CSSMathSum object whose values internal slot is set to values. return TRY(CSSMathSum::add_all_types_into_math_sum(realm, values)); } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-sub WebIDL::ExceptionOr> CSSNumericValue::sub(Vector const& initial_values) { auto& realm = this->realm(); // 1. Replace each item of values with the result of rectifying a numberish value for the item, then negating the value. Vector values; for (auto const& value : initial_values) values.append(rectify_a_numberish_value(realm, value)->negate()); // 2. Return the result of calling the add() internal algorithm with this and values. return add(values); } // https://drafts.css-houdini.org/css-typed-om-1/#cssmath-negate-a-cssnumericvalue CSSNumberish CSSNumericValue::negate() { // 1. If this is a CSSMathNegate object, return this’s value internal slot. if (auto* negate = as_if(*this)) return GC::Root { negate->value().ptr() }; // 2. If this is a CSSUnitValue object, return a new CSSUnitValue with the same unit internal slot as this, and a // value internal slot set to the negation of this’s. if (auto* unit_value = as_if(*this)) return GC::Root { CSSUnitValue::create(realm(), -unit_value->value(), unit_value->unit()).ptr() }; // 3. Otherwise, return a new CSSMathNegate object whose value internal slot is set to this. return GC::Root { CSSMathNegate::construct_impl(realm(), GC::Root { this }).ptr() }; } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-mul WebIDL::ExceptionOr> CSSNumericValue::mul(Vector const& initial_values) { auto& realm = this->realm(); // 1. Replace each item of values with the result of rectifying a numberish value for the item. // 2. If this is a CSSMathProduct object, prepend the items in this’s values internal slot to values. // Otherwise, prepend this to values. // NB: We reorder the steps a little to avoid the awkward prepending. GC::RootVector> values { heap() }; if (auto const* math_product = as_if(*this)) values.extend(math_product->values()->values()); else values.append(*this); for (auto const& value : initial_values) values.append(rectify_a_numberish_value(realm, value)); // 3. If all of the items in values are CSSUnitValues with unit internal slot set to "number", return a new // CSSUnitValue whose unit internal slot is set to "number", and value internal slot is set to the product of the // value internal slots of the items in values. // // This multiplication must be done "left to right" - if values is « 1, 2, 3, 4 », the result must be (((1 × 2) × 3) × 4). // (This detail is necessary to ensure interoperability in the presence of floating-point arithmetic.) // // 4. If all of the items in values are CSSUnitValues with unit internal slot set to "number" except one which is // set to unit, return a new CSSUnitValue whose unit internal slot is set to unit, and value internal slot is set // to the product of the value internal slots of the items in values. // // This multiplication must be done "left to right" - if values is « 1, 2, 3, 4 », the result must be (((1 × 2) × 3) × 4). bool all_values_are_units = all_of(values, [](auto& value) { return is(*value); }); if (all_values_are_units) { bool multiple_units_found = false; Optional non_number_unit_index; for (size_t i = 0; i < values.size(); ++i) { auto unit = as(*values[i]).unit(); if (unit == "number"sv) continue; if (non_number_unit_index.has_value()) { multiple_units_found = true; break; } non_number_unit_index = i; } if (!multiple_units_found) { double product = 1; for (auto& value : values) product *= as(*value).value(); auto unit = non_number_unit_index.has_value() ? as(*values[*non_number_unit_index]).unit() : "number"_fly_string; return CSSUnitValue::create(realm, product, unit); } } // 5. Let type be the result of multiplying the types of every item in values. If type is failure, throw a TypeError. // 6. Return a new CSSMathProduct object whose values internal slot is set to values. return TRY(CSSMathProduct::multiply_all_types_into_math_product(realm, values)); } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-div WebIDL::ExceptionOr> CSSNumericValue::div(Vector const& initial_values) { auto& realm = this->realm(); // 1. Replace each item of values with the result of rectifying a numberish value for the item, then inverting the value. Vector values; for (auto const& value : initial_values) values.append(TRY(rectify_a_numberish_value(realm, value)->invert())); // 2. Return the result of calling the mul() internal algorithm with this and values. return mul(values); } // https://drafts.css-houdini.org/css-typed-om-1/#cssmath-invert-a-cssnumericvalue WebIDL::ExceptionOr CSSNumericValue::invert() { // 1. If this is a CSSMathInvert object, return this’s value internal slot. if (auto* invert = as_if(*this)) return GC::Root { invert->value().ptr() }; // 2. If this is a CSSUnitValue object with unit internal slot set to "number": if (auto* unit_value = as_if(*this); unit_value && unit_value->unit() == "number"sv) { // 1. If this’s value internal slot is set to 0 or -0, throw a RangeError. if (unit_value->value() == 0 || unit_value->value() == -0) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "Zero has no multiplicative inverse"sv }; // 2. Else return a new CSSUnitValue with the unit internal slot set to "number", and a value internal slot set // to 1 divided by this’s {CSSUnitValue/value}} internal slot. return GC::Root { CSSUnitValue::create(realm(), 1.0 / unit_value->value(), "number"_fly_string).ptr() }; } // 3. Otherwise, return a new CSSMathInvert object whose value internal slot is set to this. return GC::Root { CSSMathInvert::construct_impl(realm(), GC::Root { this }).ptr() }; } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-min WebIDL::ExceptionOr> CSSNumericValue::min(Vector const& initial_values) { auto& realm = this->realm(); // 1. Replace each item of values with the result of rectifying a numberish value for the item. // 2. If this is a CSSMathMin object, prepend the items in this’s values internal slot to values. // Otherwise, prepend this to values. // NB: We reorder the steps a little to avoid the awkward prepending. GC::RootVector> values { heap() }; if (auto const* math_product = as_if(*this)) values.extend(math_product->values()->values()); else values.append(*this); for (auto const& value : initial_values) values.append(rectify_a_numberish_value(realm, value)); // 3. If all of the items in values are CSSUnitValues and have the same unit, return a new CSSUnitValue whose unit // internal slot is set to that unit, and value internal slot is set to the minimum of the value internal slots // of the items in values. if (all_values_are_css_unit_values_with_the_same_unit(values)) return apply_math_operation_on_css_unit_values(realm, values, [](double a, double b) { return AK::min(a, b); }); // 4. Let type be the result of adding the types of every item in values. If type is failure, throw a TypeError. // 5. Return a new CSSMathMin object whose values internal slot is set to values. return TRY(CSSMathMin::add_all_types_into_math_min(realm, values)); } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-max WebIDL::ExceptionOr> CSSNumericValue::max(Vector const& initial_values) { auto& realm = this->realm(); // 1. Replace each item of values with the result of rectifying a numberish value for the item. // 2. If this is a CSSMathMax object, prepend the items in this’s values internal slot to values. // Otherwise, prepend this to values. // NB: We reorder the steps a little to avoid the awkward prepending. GC::RootVector> values { heap() }; if (auto const* math_product = as_if(*this)) values.extend(math_product->values()->values()); else values.append(*this); for (auto const& value : initial_values) values.append(rectify_a_numberish_value(realm, value)); // 3. If all of the items in values are CSSUnitValues and have the same unit, return a new CSSUnitValue whose unit // internal slot is set to that unit, and value internal slot is set to the maximum of the value internal slots // of the items in values. if (all_values_are_css_unit_values_with_the_same_unit(values)) return apply_math_operation_on_css_unit_values(realm, values, [](double a, double b) { return AK::max(a, b); }); // 4. Let type be the result of adding the types of every item in values. If type is failure, throw a TypeError. // 5. Return a new CSSMathMax object whose values internal slot is set to values. return TRY(CSSMathMax::add_all_types_into_math_max(realm, values)); } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-equals bool CSSNumericValue::equals_for_bindings(Vector values) const { // The equals(...values) method, when called on a CSSNumericValue this, must perform the following steps: // 1. Replace each item of values with the result of rectifying a numberish value for the item. // 2. For each item in values, if the item is not an equal numeric value to this, return false. for (auto const& value : values) { auto rectified_value = rectify_a_numberish_value(realm(), value); if (!is_equal_numeric_value(rectified_value)) return false; } // 3. Return true. return true; } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-to WebIDL::ExceptionOr> CSSNumericValue::to(FlyString const& unit) const { // The to(unit) method converts an existing CSSNumericValue this into another one with the specified unit, if // possible. When called, it must perform the following steps: // 1. Let type be the result of creating a type from unit. If type is failure, throw a SyntaxError. auto maybe_type = NumericType::create_from_unit(unit); if (!maybe_type.has_value()) return WebIDL::SyntaxError::create(realm(), Utf16String::formatted("Unrecognized unit '{}'", unit)); // 2. Let sum be the result of creating a sum value from this. If sum is failure, throw a TypeError. auto sum = create_a_sum_value(); if (!sum.has_value()) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unable to create a sum from input '{}'", MUST(to_string()))) }; // 3. If sum has more than one item, throw a TypeError. // Otherwise, let item be the result of creating a CSSUnitValue from the sole item in sum, then converting it to // unit. If item is failure, throw a TypeError. if (sum->size() > 1) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Sum contains more than one item"sv }; auto item = CSSUnitValue::create_from_sum_value_item(realm(), sum->first()); if (!item) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unable to create CSSUnitValue from input '{}'", MUST(to_string()))) }; auto converted_item = item->converted_to_unit(unit); if (!converted_item) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unable to convert input '{}' to unit '{}'", MUST(to_string()), unit)) }; // 4. Return item. return converted_item.as_nonnull(); } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-type CSSNumericType CSSNumericValue::type_for_bindings() const { // 1. Let result be a new CSSNumericType. CSSNumericType result {}; // 2. For each baseType → power in the type of this, m_type.for_each_type_and_exponent([&result](NumericType::BaseType base_type, auto power) { // 1. If power is not 0, set result[baseType] to power. if (power == 0) return; switch (base_type) { case NumericType::BaseType::Length: result.length = power; break; case NumericType::BaseType::Angle: result.angle = power; break; case NumericType::BaseType::Time: result.time = power; break; case NumericType::BaseType::Frequency: result.frequency = power; break; case NumericType::BaseType::Resolution: result.resolution = power; break; case NumericType::BaseType::Flex: result.flex = power; break; case NumericType::BaseType::Percent: result.percent = power; break; case NumericType::BaseType::__Count: VERIFY_NOT_REACHED(); } }); // 3. If the percent hint of this is not null, if (auto percent_hint = m_type.percent_hint(); percent_hint.has_value()) { // 1. Set result[percentHint] to the percent hint of this. result.percent_hint = to_om_numeric_base_type(percent_hint.value()); } // 4. Return result. return result; } // https://drafts.css-houdini.org/css-typed-om-1/#serialize-a-cssnumericvalue void CSSNumericValue::serialize(StringBuilder& builder, SerializationParams const& params) const { // To serialize a CSSNumericValue this, given an optional minimum, a numeric value, and optional maximum, a numeric value: // 1. If this is a CSSUnitValue, serialize a CSSUnitValue from this, passing minimum and maximum. Return the result. if (auto* unit_value = as_if(this)) { unit_value->serialize_unit_value(builder, params.minimum, params.maximum); return; } // 2. Otherwise, serialize a CSSMathValue from this, and return the result. auto& math_value = as(*this); math_value.serialize_math_value(builder, params.nested ? CSSMathValue::Nested::Yes : CSSMathValue::Nested::No, params.parenless ? CSSMathValue::Parens::Without : CSSMathValue::Parens::With); } String CSSNumericValue::to_string(SerializationParams const& params) const { StringBuilder builder; serialize(builder, params); return builder.to_string_without_validation(); } // https://drafts.css-houdini.org/css-typed-om-1/#rectify-a-numberish-value GC::Ref rectify_a_numberish_value(JS::Realm& realm, CSSNumberish const& numberish, Optional unit) { // To rectify a numberish value num, optionally to a given unit unit (defaulting to "number"), perform the following steps: return numberish.visit( // 1. If num is a CSSNumericValue, return num. [](GC::Root const& num) -> GC::Ref { return GC::Ref { *num }; }, // 2. If num is a double, return a new CSSUnitValue with its value internal slot set to num and its unit // internal slot set to unit. [&realm, &unit](double num) -> GC::Ref { return CSSUnitValue::create(realm, num, unit.value_or("number"_fly_string)); }); } // https://drafts.css-houdini.org/css-typed-om-1/#reify-a-numeric-value static WebIDL::ExceptionOr> reify_a_numeric_value(JS::Realm& realm, Parser::ComponentValue const& numeric_value) { // To reify a numeric value num: // 1. If num is a math function, reify a math expression from num and return the result. if (numeric_value.is_function()) { // AD-HOC: The only feasible way is to parse it as a StyleValue and rely on the reification code there. auto parser = Parser::Parser::create(Parser::ParsingParams {}, {}); if (auto calculation = parser.parse_calculated_value(numeric_value)) { auto reified = calculation->reify(realm, {}); // AD-HOC: Not all math functions can be reified. Until we have clear guidance on that, throw a SyntaxError. // See: https://github.com/w3c/css-houdini-drafts/issues/1090#issuecomment-3200229996 if (auto* reified_numeric = as_if(*reified)) { return GC::Ref { *reified_numeric }; } return WebIDL::SyntaxError::create(realm, "Unable to reify this math function."_utf16); } // AD-HOC: If we failed to parse it, I guess we throw a SyntaxError like in step 1 of CSSNumericValue::parse(). return WebIDL::SyntaxError::create(realm, "Unable to parse input as a calculation tree."_utf16); } // 2. If num is the unitless value 0 and num is a , return a new CSSUnitValue with its value internal // slot set to 0, and its unit internal slot set to "px". // FIXME: What does this mean? We just have a component value, it doesn't have any knowledge about whether 0 should // be interpreted as a dimension. // 3. Return a new CSSUnitValue with its value internal slot set to the numeric value of num, and its unit internal // slot set to "number" if num is a , "percent" if num is a , and num’s unit if num is a // . // If the value being reified is a computed value, the unit used must be the appropriate canonical unit for the // value’s type, with the numeric value scaled accordingly. // NB: The computed value part is irrelevant here, I think. if (numeric_value.is(Parser::Token::Type::Number)) return CSSUnitValue::create(realm, numeric_value.token().number_value(), "number"_fly_string); if (numeric_value.is(Parser::Token::Type::Percentage)) return CSSUnitValue::create(realm, numeric_value.token().percentage(), "percent"_fly_string); VERIFY(numeric_value.is(Parser::Token::Type::Dimension)); return CSSUnitValue::create(realm, numeric_value.token().dimension_value(), numeric_value.token().dimension_unit()); } // https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-parse WebIDL::ExceptionOr> CSSNumericValue::parse(JS::VM& vm, String const& css_text) { // The parse(cssText) method, when called, must perform the following steps: auto& realm = *vm.current_realm(); // 1. Parse a component value from cssText and let result be the result. If result is a syntax error, throw a // SyntaxError and abort this algorithm. auto maybe_component_value = Parser::Parser::create(Parser::ParsingParams {}, css_text).parse_as_component_value(); if (!maybe_component_value.has_value()) { return WebIDL::SyntaxError::create(realm, "Unable to parse input as a component value."_utf16); } auto& result = maybe_component_value.value(); // 2. If result is not a , , , or a math function, throw a // SyntaxError and abort this algorithm. auto is_a_math_function = [](Parser::ComponentValue const& component_value) -> bool { if (!component_value.is_function()) return false; return math_function_from_string(component_value.function().name).has_value(); }; if (!(result.is(Parser::Token::Type::Number) || result.is(Parser::Token::Type::Percentage) || result.is(Parser::Token::Type::Dimension) || is_a_math_function(result))) { return WebIDL::SyntaxError::create(realm, "Input not a , , , or a math function."_utf16); } // 3. If result is a and creating a type from result’s unit returns failure, throw a SyntaxError // and abort this algorithm. if (result.is(Parser::Token::Type::Dimension)) { if (!NumericType::create_from_unit(result.token().dimension_unit()).has_value()) { return WebIDL::SyntaxError::create(realm, "Input is with an unrecognized unit."_utf16); } } // 4. Reify a numeric value result, and return the result. return reify_a_numeric_value(realm, result); } }