LibWeb/CSS: Reify UnresolvedStyleValue as CSSUnparsedValue

This commit is contained in:
Sam Atkins 2025-08-14 17:25:29 +01:00
commit 0fc512e56d
Notes: github-actions[bot] 2025-08-21 09:23:05 +00:00
5 changed files with 132 additions and 9 deletions

View file

@ -8,6 +8,10 @@
*/
#include <AK/StringBuilder.h>
#include <LibWeb/CSS/CSSUnparsedValue.h>
#include <LibWeb/CSS/CSSVariableReferenceValue.h>
#include <LibWeb/CSS/Parser/ArbitrarySubstitutionFunctions.h>
#include <LibWeb/CSS/PropertyName.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>
@ -51,4 +55,120 @@ bool UnresolvedStyleValue::equals(StyleValue const& other) const
return values() == other.as_unresolved().values();
}
static GC::Ref<CSSUnparsedValue> reify_a_list_of_component_values(JS::Realm&, Vector<Parser::ComponentValue>);
// https://drafts.css-houdini.org/css-typed-om-1/#reify-var
static GC::Root<CSSVariableReferenceValue> 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 objects variable internal slot to the serialization of the <custom-ident> providing the variable name.
FlyString variable = maybe_variable.token().ident();
// 3. If var has a fallback value, set objects fallback internal slot to the result of reifying the fallbacks
// component values. Otherwise, set it to null.
GC::Ptr<CSSUnparsedValue> 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<GCRootCSSUnparsedSegment> reify(JS::Realm& realm, Vector<Parser::ComponentValue> 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<Parser::ComponentValue> 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<GCRootCSSUnparsedSegment> m_reified_values {};
Vector<Parser::ComponentValue> m_unserialized_values {};
};
static GC::Ref<CSSUnparsedValue> reify_a_list_of_component_values(JS::Realm& realm, Vector<Parser::ComponentValue> 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<CSSStyleValue> UnresolvedStyleValue::reify(JS::Realm& realm, String const&) const
{
return reify_a_list_of_component_values(realm, m_values);
}
}

View file

@ -30,6 +30,8 @@ public:
virtual bool equals(StyleValue const& other) const override;
virtual GC::Ref<CSSStyleValue> reify(JS::Realm&, String const& associated_property) const override;
private:
UnresolvedStyleValue(Vector<Parser::ComponentValue>&& values, Parser::SubstitutionFunctionsPresence, Optional<String> original_source_text);

View file

@ -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

View file

@ -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

View file

@ -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