ladybird/Libraries/LibWeb/CSS/CSSNumericValue.cpp

526 lines
25 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2026, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibWeb/Bindings/CSSNumericValuePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSMathInvert.h>
#include <LibWeb/CSS/CSSMathMax.h>
#include <LibWeb/CSS/CSSMathMin.h>
#include <LibWeb/CSS/CSSMathNegate.h>
#include <LibWeb/CSS/CSSMathProduct.h>
#include <LibWeb/CSS/CSSMathSum.h>
#include <LibWeb/CSS/CSSMathValue.h>
#include <LibWeb/CSS/CSSNumericArray.h>
#include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/MathFunctions.h>
#include <LibWeb/CSS/NumericType.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
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<GC::Ref<CSSNumericValue>> const& values)
{
VERIFY(!values.is_empty());
return all_of(values, [&](auto& value) {
if (auto* unit_value = as_if<CSSUnitValue>(*value))
return unit_value->unit() == as<CSSUnitValue>(*values[0]).unit();
return false;
});
}
template<typename Operation>
static GC::Ref<CSSNumericValue> apply_math_operation_on_css_unit_values(JS::Realm& realm, GC::RootVector<GC::Ref<CSSNumericValue>> const& values, Operation&& operation)
{
auto& first_unit_value = as<CSSUnitValue>(*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<CSSUnitValue>(*values[i]).value());
return CSSUnitValue::create(realm, result, unit);
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-add
WebIDL::ExceptionOr<GC::Ref<CSSNumericValue>> CSSNumericValue::add(Vector<CSSNumberish> 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 thiss values internal slot to values.
// Otherwise, prepend this to values.
// NB: We reorder the steps a little to avoid the awkward prepending.
GC::RootVector<GC::Ref<CSSNumericValue>> values { heap() };
if (auto const* math_sum = as_if<CSSMathSum>(*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<GC::Ref<CSSNumericValue>> CSSNumericValue::sub(Vector<CSSNumberish> 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<CSSNumberish> 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 thiss value internal slot.
if (auto* negate = as_if<CSSMathNegate>(*this))
return GC::Root<CSSNumericValue> { 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 thiss.
if (auto* unit_value = as_if<CSSUnitValue>(*this))
return GC::Root<CSSNumericValue> { 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<CSSNumericValue> { CSSMathNegate::construct_impl(realm(), GC::Root<CSSNumericValue> { this }).ptr() };
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-mul
WebIDL::ExceptionOr<GC::Ref<CSSNumericValue>> CSSNumericValue::mul(Vector<CSSNumberish> 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 thiss values internal slot to values.
// Otherwise, prepend this to values.
// NB: We reorder the steps a little to avoid the awkward prepending.
GC::RootVector<GC::Ref<CSSNumericValue>> values { heap() };
if (auto const* math_product = as_if<CSSMathProduct>(*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<CSSUnitValue>(*value);
});
if (all_values_are_units) {
bool multiple_units_found = false;
Optional<size_t> non_number_unit_index;
for (size_t i = 0; i < values.size(); ++i) {
auto unit = as<CSSUnitValue>(*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<CSSUnitValue>(*value).value();
auto unit = non_number_unit_index.has_value() ? as<CSSUnitValue>(*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<GC::Ref<CSSNumericValue>> CSSNumericValue::div(Vector<CSSNumberish> 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<CSSNumberish> 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<CSSNumberish> CSSNumericValue::invert()
{
// 1. If this is a CSSMathInvert object, return thiss value internal slot.
if (auto* invert = as_if<CSSMathInvert>(*this))
return GC::Root<CSSNumericValue> { invert->value().ptr() };
// 2. If this is a CSSUnitValue object with unit internal slot set to "number":
if (auto* unit_value = as_if<CSSUnitValue>(*this); unit_value && unit_value->unit() == "number"sv) {
// 1. If thiss 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 thiss {CSSUnitValue/value}} internal slot.
return GC::Root<CSSNumericValue> { 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<CSSNumericValue> { CSSMathInvert::construct_impl(realm(), GC::Root<CSSNumericValue> { this }).ptr() };
}
// https://drafts.css-houdini.org/css-typed-om-1/#dom-cssnumericvalue-min
WebIDL::ExceptionOr<GC::Ref<CSSNumericValue>> CSSNumericValue::min(Vector<CSSNumberish> 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 thiss values internal slot to values.
// Otherwise, prepend this to values.
// NB: We reorder the steps a little to avoid the awkward prepending.
GC::RootVector<GC::Ref<CSSNumericValue>> values { heap() };
if (auto const* math_product = as_if<CSSMathMin>(*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<GC::Ref<CSSNumericValue>> CSSNumericValue::max(Vector<CSSNumberish> 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 thiss values internal slot to values.
// Otherwise, prepend this to values.
// NB: We reorder the steps a little to avoid the awkward prepending.
GC::RootVector<GC::Ref<CSSNumericValue>> values { heap() };
if (auto const* math_product = as_if<CSSMathMax>(*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<CSSNumberish> 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<GC::Ref<CSSUnitValue>> 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<CSSUnitValue>(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<CSSMathValue>(*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<CSSNumericValue> rectify_a_numberish_value(JS::Realm& realm, CSSNumberish const& numberish, Optional<FlyString> 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<CSSNumericValue> const& num) -> GC::Ref<CSSNumericValue> {
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<CSSNumericValue> {
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<GC::Ref<CSSNumericValue>> 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<CSSNumericValue>(*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 <dimension>, 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 <number>, "percent" if num is a <percentage>, and nums unit if num is a
// <dimension>.
// If the value being reified is a computed value, the unit used must be the appropriate canonical unit for the
// values 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<GC::Ref<CSSNumericValue>> 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 <number-token>, <percentage-token>, <dimension-token>, 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 <number-token>, <percentage-token>, <dimension-token>, or a math function."_utf16);
}
// 3. If result is a <dimension-token> and creating a type from results 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 <dimension> with an unrecognized unit."_utf16);
}
}
// 4. Reify a numeric value result, and return the result.
return reify_a_numeric_value(realm, result);
}
}