ladybird/Libraries/LibWeb/CSS/CSSNumericValue.cpp
Sam Atkins d3d695e9d2 LibWeb/CSS: Make CSSStyleValue.to_string() return ExceptionOr
DOMMatrix.to_string() throws exceptions if any of its values are
non-finite. This ends up affecting CSSStyleValue because its subclass
CSSTransformValue (which is about to be added) serializes
CSSTransformComponents, and one of those is CSSMatrixComponent, which
calls DOMMatrix.to_string().

This is all quite unfortunate, and because at the time the spec for
DOMMatrix was written, CSS couldn't represent NaN or infinity. That's
no longer true, so I'm hoping the spec can be updated and this can be
reverted. https://github.com/w3c/fxtf-drafts/issues/611
2025-09-24 12:27:05 +01:00

268 lines
12 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>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "CSSNumericValue.h"
#include <LibWeb/Bindings/CSSNumericValuePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSMathValue.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);
}
// 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
String CSSNumericValue::to_string(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)) {
return unit_value->serialize_unit_value(params.minimum, params.maximum);
}
// 2. Otherwise, serialize a CSSMathValue from this, and return the result.
auto& math_value = as<CSSMathValue>(*this);
return math_value.serialize_math_value(
params.nested ? CSSMathValue::Nested::Yes : CSSMathValue::Nested::No,
params.parenless ? CSSMathValue::Parens::Without : CSSMathValue::Parens::With);
}
// 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);
}
}