LibWeb/CSS: Implement CSSNumericValue.parse()

Reifying the result gets quite ad-hoc. Firstly because "parse a
component value" produces a ComponentValue, not a full StyleValue like
we need for math functions. And second, because not all math functions
can be reified as a CSSNumericValue:

Besides the fact that I haven't implemented CalculatedStyleValue
reification at all yet, there are a lot of math functions with no
corresponding CSSMathValue in the spec yet. If the calculation tree
contains any of those, the best we can do is reify as a CSSStyleValue,
and that isn't a valid return value from CSSNumericValue.parse(). So, I
made us throw a SyntaxError in those cases. This seems to match
Chrome's behaviour. Spec issue:
https://github.com/w3c/css-houdini-drafts/issues/1090
This commit is contained in:
Sam Atkins 2025-08-21 16:20:24 +01:00 committed by Andreas Kling
commit 277117eed5
Notes: github-actions[bot] 2025-08-29 09:58:54 +00:00
6 changed files with 104 additions and 17 deletions

View file

@ -9,8 +9,11 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSMathValue.h> #include <LibWeb/CSS/CSSMathValue.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/MathFunctions.h>
#include <LibWeb/CSS/NumericType.h> #include <LibWeb/CSS/NumericType.h>
#include <LibWeb/CSS/Serialize.h> #include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::CSS { namespace Web::CSS {
@ -131,4 +134,85 @@ GC::Ref<CSSNumericValue> rectify_a_numberish_value(JS::Realm& realm, CSSNumberis
}); });
} }
// 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);
}
} }

View file

@ -45,6 +45,8 @@ public:
virtual String to_string() const final override { return to_string({}); } virtual String to_string() const final override { return to_string({}); }
String to_string(SerializationParams const&) const; String to_string(SerializationParams const&) const;
static WebIDL::ExceptionOr<GC::Ref<CSSNumericValue>> parse(JS::VM&, String const& css_text);
protected: protected:
explicit CSSNumericValue(JS::Realm&, NumericType); explicit CSSNumericValue(JS::Realm&, NumericType);

View file

@ -40,7 +40,7 @@ interface CSSNumericValue : CSSStyleValue {
// FIXME: CSSMathSum toSum(USVString... units); // FIXME: CSSMathSum toSum(USVString... units);
[ImplementedAs=type_for_bindings] CSSNumericType type(); [ImplementedAs=type_for_bindings] CSSNumericType type();
[FIXME, Exposed=Window] static CSSNumericValue parse(USVString cssText); [Exposed=Window] static CSSNumericValue parse(USVString cssText);
}; };
// https://drafts.css-houdini.org/css-typed-om-1/#typedefdef-cssnumberish // https://drafts.css-houdini.org/css-typed-om-1/#typedefdef-cssnumberish

View file

@ -146,6 +146,8 @@ public:
NonnullRefPtr<StyleValue const> parse_with_a_syntax(Vector<ComponentValue> const& input, SyntaxNode const& syntax, Optional<DOM::AbstractElement> const& element = {}); NonnullRefPtr<StyleValue const> parse_with_a_syntax(Vector<ComponentValue> const& input, SyntaxNode const& syntax, Optional<DOM::AbstractElement> const& element = {});
RefPtr<StyleValue const> parse_calculated_value(ComponentValue const&);
private: private:
Parser(ParsingParams const&, Vector<Token>); Parser(ParsingParams const&, Vector<Token>);
@ -350,7 +352,6 @@ private:
}; };
Optional<PropertyAndValue> parse_css_value_for_properties(ReadonlySpan<PropertyID>, TokenStream<ComponentValue>&); Optional<PropertyAndValue> parse_css_value_for_properties(ReadonlySpan<PropertyID>, TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_builtin_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_builtin_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_calculated_value(ComponentValue const&);
Optional<FlyString> parse_custom_ident(TokenStream<ComponentValue>&, ReadonlySpan<StringView> blacklist); Optional<FlyString> parse_custom_ident(TokenStream<ComponentValue>&, ReadonlySpan<StringView> blacklist);
RefPtr<CustomIdentStyleValue const> parse_custom_ident_value(TokenStream<ComponentValue>&, ReadonlySpan<StringView> blacklist); RefPtr<CustomIdentStyleValue const> parse_custom_ident_value(TokenStream<ComponentValue>&, ReadonlySpan<StringView> blacklist);
Optional<FlyString> parse_dashed_ident(TokenStream<ComponentValue>&); Optional<FlyString> parse_dashed_ident(TokenStream<ComponentValue>&);

View file

@ -2,11 +2,11 @@ Harness status: OK
Found 6 tests Found 6 tests
1 Pass 5 Pass
5 Fail 1 Fail
Fail Normalizing a <number> returns a number CSSUnitValue Pass Normalizing a <number> returns a number CSSUnitValue
Fail Normalizing a <percentage> returns a percent CSSUnitValue Pass Normalizing a <percentage> returns a percent CSSUnitValue
Fail Normalizing a <dimension> returns a CSSUnitValue with the correct unit Pass Normalizing a <dimension> returns a CSSUnitValue with the correct unit
Fail Normalizing a <number> with a unitless zero returns 0 Pass Normalizing a <number> with a unitless zero returns 0
Fail Normalizing a <calc> returns simplified expression Fail Normalizing a <calc> returns simplified expression
Pass Normalizing a <dimension> with a unitless zero returns 0px Pass Normalizing a <dimension> with a unitless zero returns 0px

View file

@ -2,14 +2,14 @@ Harness status: OK
Found 11 tests Found 11 tests
2 Pass 8 Pass
9 Fail 3 Fail
Fail Parsing an invalid string throws SyntaxError Pass Parsing an invalid string throws SyntaxError
Fail Parsing a string with a non numeric token throws SyntaxError Pass Parsing a string with a non numeric token throws SyntaxError
Fail Parsing a string with left over numeric tokens throws SyntaxError Pass Parsing a string with left over numeric tokens throws SyntaxError
Fail Parsing a calc with incompatible units throws a SyntaxError Pass Parsing a calc with incompatible units throws a SyntaxError
Fail Parsing a <dimension-token> with invalid units throws a SyntaxError Pass Parsing a <dimension-token> with invalid units throws a SyntaxError
Fail Parsing ignores surrounding spaces Pass Parsing ignores surrounding spaces
Fail Parsing min() is successful Fail Parsing min() is successful
Fail Parsing max() is successful Fail Parsing max() is successful
Fail Parsing clamp() is successful Fail Parsing clamp() is successful