From 277117eed53ea2888b56aa3b10f313148dce7378 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 21 Aug 2025 16:20:24 +0100 Subject: [PATCH] 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 --- Libraries/LibWeb/CSS/CSSNumericValue.cpp | 86 ++++++++++++++++++- Libraries/LibWeb/CSS/CSSNumericValue.h | 2 + Libraries/LibWeb/CSS/CSSNumericValue.idl | 2 +- Libraries/LibWeb/CSS/Parser/Parser.h | 3 +- .../normalize-numeric.tentative.txt | 12 +-- .../numeric-objects/parse.tentative.txt | 16 ++-- 6 files changed, 104 insertions(+), 17 deletions(-) diff --git a/Libraries/LibWeb/CSS/CSSNumericValue.cpp b/Libraries/LibWeb/CSS/CSSNumericValue.cpp index 4002af5cfff..d839aea47f1 100644 --- a/Libraries/LibWeb/CSS/CSSNumericValue.cpp +++ b/Libraries/LibWeb/CSS/CSSNumericValue.cpp @@ -9,8 +9,11 @@ #include #include #include +#include #include -#include +#include +#include +#include namespace Web::CSS { @@ -131,4 +134,85 @@ GC::Ref rectify_a_numberish_value(JS::Realm& realm, CSSNumberis }); } +// 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); +} + } diff --git a/Libraries/LibWeb/CSS/CSSNumericValue.h b/Libraries/LibWeb/CSS/CSSNumericValue.h index 79c6fb2feaf..739ef8980fc 100644 --- a/Libraries/LibWeb/CSS/CSSNumericValue.h +++ b/Libraries/LibWeb/CSS/CSSNumericValue.h @@ -45,6 +45,8 @@ public: virtual String to_string() const final override { return to_string({}); } String to_string(SerializationParams const&) const; + static WebIDL::ExceptionOr> parse(JS::VM&, String const& css_text); + protected: explicit CSSNumericValue(JS::Realm&, NumericType); diff --git a/Libraries/LibWeb/CSS/CSSNumericValue.idl b/Libraries/LibWeb/CSS/CSSNumericValue.idl index e61e2d3b96e..3bc3b5c5ede 100644 --- a/Libraries/LibWeb/CSS/CSSNumericValue.idl +++ b/Libraries/LibWeb/CSS/CSSNumericValue.idl @@ -40,7 +40,7 @@ interface CSSNumericValue : CSSStyleValue { // FIXME: CSSMathSum toSum(USVString... units); [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 diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index f8a96fa3627..42c285e16b1 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -146,6 +146,8 @@ public: NonnullRefPtr parse_with_a_syntax(Vector const& input, SyntaxNode const& syntax, Optional const& element = {}); + RefPtr parse_calculated_value(ComponentValue const&); + private: Parser(ParsingParams const&, Vector); @@ -350,7 +352,6 @@ private: }; Optional parse_css_value_for_properties(ReadonlySpan, TokenStream&); RefPtr parse_builtin_value(TokenStream&); - RefPtr parse_calculated_value(ComponentValue const&); Optional parse_custom_ident(TokenStream&, ReadonlySpan blacklist); RefPtr parse_custom_ident_value(TokenStream&, ReadonlySpan blacklist); Optional parse_dashed_ident(TokenStream&); diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-normalization/normalize-numeric.tentative.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-normalization/normalize-numeric.tentative.txt index 615e09706f9..e8cda78920f 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-normalization/normalize-numeric.tentative.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-normalization/normalize-numeric.tentative.txt @@ -2,11 +2,11 @@ Harness status: OK Found 6 tests -1 Pass -5 Fail -Fail Normalizing a returns a number CSSUnitValue -Fail Normalizing a returns a percent CSSUnitValue -Fail Normalizing a returns a CSSUnitValue with the correct unit -Fail Normalizing a with a unitless zero returns 0 +5 Pass +1 Fail +Pass Normalizing a returns a number CSSUnitValue +Pass Normalizing a returns a percent CSSUnitValue +Pass Normalizing a returns a CSSUnitValue with the correct unit +Pass Normalizing a with a unitless zero returns 0 Fail Normalizing a returns simplified expression Pass Normalizing a with a unitless zero returns 0px \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-subclasses/numeric-objects/parse.tentative.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-subclasses/numeric-objects/parse.tentative.txt index 734e1ec2fcd..201387869c4 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-subclasses/numeric-objects/parse.tentative.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/stylevalue-subclasses/numeric-objects/parse.tentative.txt @@ -2,14 +2,14 @@ Harness status: OK Found 11 tests -2 Pass -9 Fail -Fail Parsing an invalid string throws SyntaxError -Fail Parsing a string with a non numeric token throws SyntaxError -Fail Parsing a string with left over numeric tokens throws SyntaxError -Fail Parsing a calc with incompatible units throws a SyntaxError -Fail Parsing a with invalid units throws a SyntaxError -Fail Parsing ignores surrounding spaces +8 Pass +3 Fail +Pass Parsing an invalid string throws SyntaxError +Pass Parsing a string with a non numeric token throws SyntaxError +Pass Parsing a string with left over numeric tokens throws SyntaxError +Pass Parsing a calc with incompatible units throws a SyntaxError +Pass Parsing a with invalid units throws a SyntaxError +Pass Parsing ignores surrounding spaces Fail Parsing min() is successful Fail Parsing max() is successful Fail Parsing clamp() is successful