From 0fc512e56dd6df58743b28bbf8ac16566d026d57 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 14 Aug 2025 17:25:29 +0100 Subject: [PATCH] LibWeb/CSS: Reify UnresolvedStyleValue as CSSUnparsedValue --- .../CSS/StyleValues/UnresolvedStyleValue.cpp | 120 ++++++++++++++++++ .../CSS/StyleValues/UnresolvedStyleValue.h | 2 + .../computed/computed.tentative.txt | 7 +- .../the-stylepropertymap/computed/get.txt | 6 +- .../computed/getAll.tentative.txt | 6 +- 5 files changed, 132 insertions(+), 9 deletions(-) diff --git a/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.cpp index a532278f60e..531bc845469 100644 --- a/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.cpp @@ -8,6 +8,10 @@ */ #include +#include +#include +#include +#include #include #include @@ -51,4 +55,120 @@ bool UnresolvedStyleValue::equals(StyleValue const& other) const return values() == other.as_unresolved().values(); } +static GC::Ref reify_a_list_of_component_values(JS::Realm&, Vector); + +// https://drafts.css-houdini.org/css-typed-om-1/#reify-var +static GC::Root reify_a_var_reference(JS::Realm& realm, Parser::Function function) +{ + // NB: A var() might not be representable as a CSSVariableReferenceValue, for example if it has invalid syntax or + // it contains an ASF in its variable-name slot. In those cases, we return null here, so it's treated like a + // regular function. + auto maybe_var_arguments = Parser::parse_according_to_argument_grammar(Parser::ArbitrarySubstitutionFunction::Var, function.value); + if (!maybe_var_arguments.has_value()) + return nullptr; + auto var_arguments = maybe_var_arguments.release_value(); + // NB: Try to parse the variable name. If we can't, return null as above. + + Parser::TokenStream tokens { var_arguments.first() }; + tokens.discard_whitespace(); + auto& maybe_variable = tokens.consume_a_token(); + tokens.discard_whitespace(); + if (tokens.has_next_token() + || !maybe_variable.is(Parser::Token::Type::Ident) + || !is_a_custom_property_name_string(maybe_variable.token().ident())) + return nullptr; + + // To reify a var() reference var: + // 1. Let object be a new CSSVariableReferenceValue. + + // 2. Set object’s variable internal slot to the serialization of the providing the variable name. + FlyString variable = maybe_variable.token().ident(); + + // 3. If var has a fallback value, set object’s fallback internal slot to the result of reifying the fallback’s + // component values. Otherwise, set it to null. + GC::Ptr fallback; + if (var_arguments.size() > 1) + fallback = reify_a_list_of_component_values(realm, var_arguments[1]); + + // 4. Return object. + return CSSVariableReferenceValue::create(realm, move(variable), move(fallback)); +} + +class Reifier { +public: + static Vector reify(JS::Realm& realm, Vector const& source_values) + { + Reifier reifier; + reifier.process_values(realm, source_values); + if (!reifier.m_unserialized_values.is_empty()) + reifier.serialize_unserialized_values(); + return move(reifier.m_reified_values); + } + +private: + void process_values(JS::Realm& realm, Vector const& source_values) + { + // NB: var() could be arbitrarily nested within other functions and blocks, so we have to walk the tree. + // Also, a var() might not be representable, if it has an ASF in place of its name, so those will be part + // of a string instead. + for (auto const& component_value : source_values) { + if (component_value.is_function("var"sv)) { + // First parse the var() to see if it is representable as a CSSVariableReferenceValue. It might not be, + // for example if it has an ASF in the place of its variable name. In that case we fall back to + // serializing it like a regular function. + if (auto var_reference = reify_a_var_reference(realm, component_value.function())) { + serialize_unserialized_values(); + m_reified_values.append(move(var_reference)); + continue; + } + } + + if (component_value.is_function()) { + auto& function = component_value.function(); + m_unserialized_values.append(function.name_token); + process_values(realm, function.value); + m_unserialized_values.append(function.end_token); + continue; + } + + if (component_value.is_block()) { + auto& block = component_value.block(); + m_unserialized_values.append(block.token); + process_values(realm, block.value); + m_unserialized_values.append(block.end_token); + continue; + } + + m_unserialized_values.append(component_value); + } + } + + void serialize_unserialized_values() + { + // FIXME: Stop inserting whitespace once we stop removing it during parsing. + m_reified_values.append(serialize_a_series_of_component_values(m_unserialized_values, InsertWhitespace::Yes)); + m_unserialized_values.clear_with_capacity(); + } + + Vector m_reified_values {}; + Vector m_unserialized_values {}; +}; + +static GC::Ref reify_a_list_of_component_values(JS::Realm& realm, Vector component_values) +{ + // To reify a list of component values from a list: + // 1. Replace all var() references in list with CSSVariableReferenceValue objects, as described in §5.4 var() References. + // 2. Replace each remaining maximal subsequence of component values in list with a single string of their concatenated serializations. + auto reified_values = Reifier::reify(realm, component_values); + + // 3. Return a new CSSUnparsedValue whose [[tokens]] slot is set to list. + return CSSUnparsedValue::create(realm, move(reified_values)); +} + +// https://drafts.css-houdini.org/css-typed-om-1/#reify-a-list-of-component-values +GC::Ref UnresolvedStyleValue::reify(JS::Realm& realm, String const&) const +{ + return reify_a_list_of_component_values(realm, m_values); +} + } diff --git a/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.h index 1a5ae596dcf..528eb6c03d8 100644 --- a/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/UnresolvedStyleValue.h @@ -30,6 +30,8 @@ public: virtual bool equals(StyleValue const& other) const override; + virtual GC::Ref reify(JS::Realm&, String const& associated_property) const override; + private: UnresolvedStyleValue(Vector&& values, Parser::SubstitutionFunctionsPresence, Optional original_source_text); diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/computed.tentative.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/computed.tentative.txt index a4ccde9d274..47c504d3e32 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/computed.tentative.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/computed.tentative.txt @@ -2,11 +2,12 @@ Harness status: OK Found 7 tests -7 Fail +2 Pass +5 Fail Fail Computed StylePropertyMap contains every CSS property Fail Computed StylePropertyMap contains CSS property declarations in style rules -Fail Computed StylePropertyMap contains custom property declarations in style rules +Pass Computed StylePropertyMap contains custom property declarations in style rules Fail Computed StylePropertyMap contains CSS property declarations in inline styles -Fail Computed StylePropertyMap contains custom property declarations in inline rules +Pass Computed StylePropertyMap contains custom property declarations in inline rules Fail Computed StylePropertyMap contains computed values and not resolved values Fail Computed StylePropertyMap is live \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/get.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/get.txt index d3ccc61ecf8..e7eabac5fa8 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/get.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/get.txt @@ -2,11 +2,11 @@ Harness status: OK Found 6 tests -1 Pass -5 Fail +2 Pass +4 Fail Pass Getting a custom property not in the computed style returns undefined Fail Getting a valid property from computed style returns the correct entry -Fail Getting a valid custom property from computed style returns the correct entry +Pass Getting a valid custom property from computed style returns the correct entry Fail Getting a list-valued property from computed style returns only the first value Fail Computed StylePropertyMap.get is not case-sensitive Fail Computed StylePropertyMap.get reflects updates in inline style \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/getAll.tentative.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/getAll.tentative.txt index 9473e7be119..14fdfee5c9e 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/getAll.tentative.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-typed-om/the-stylepropertymap/computed/getAll.tentative.txt @@ -2,11 +2,11 @@ Harness status: OK Found 6 tests -2 Pass -4 Fail +3 Pass +3 Fail Pass Calling StylePropertyMap.getAll with an unsupported property throws a TypeError Pass Calling StylePropertyMap.getAll with a custom property not in the property model returns an empty list Fail Calling StylePropertyMap.getAll with a valid property returns a single element list with the correct entry Fail StylePropertyMap.getAll is case-insensitive -Fail Calling StylePropertyMap.getAll with a valid custom property returns a single element list with the correct entry +Pass Calling StylePropertyMap.getAll with a valid custom property returns a single element list with the correct entry Fail Calling StylePropertyMap.getAll with a list-valued property returns all the values \ No newline at end of file