LibWeb: Parse the scroll-timeline shorthand CSS property

The remaining failing tests in scroll-timeline-shorthand.html are due to
either:
 a) incorrect tests, see web-platform-tests/wpt#56181 or;
 b) a wider issue where we collapse coordinating value list longhand
properties to a single value when we shouldn't.
This commit is contained in:
Callum Law 2025-11-21 01:30:02 +13:00 committed by Sam Atkins
parent 992b0a4dc6
commit 13ce2d1857
Notes: github-actions[bot] 2025-11-28 13:26:23 +00:00
8 changed files with 261 additions and 3 deletions

View file

@ -499,6 +499,7 @@ private:
RefPtr<StyleValue const> parse_position_visibility_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_quotes_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_single_repeat_style_value(PropertyID, TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_scroll_timeline_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_scrollbar_color_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_scrollbar_gutter_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_shadow_value(TokenStream<ComponentValue>&, ShadowStyleValue::ShadowType);

View file

@ -745,6 +745,8 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_css_value(Pr
return parse_all_as(tokens, [this](auto& tokens) { return parse_translate_value(tokens); });
case PropertyID::Scale:
return parse_all_as(tokens, [this](auto& tokens) { return parse_scale_value(tokens); });
case PropertyID::ScrollTimeline:
return parse_all_as(tokens, [this](auto& tokens) { return parse_scroll_timeline_value(tokens); });
case PropertyID::ScrollTimelineAxis:
case PropertyID::ScrollTimelineName:
return parse_all_as(tokens, [this, property_id](auto& tokens) { return parse_simple_comma_separated_value_list(property_id, tokens); });
@ -5182,6 +5184,73 @@ RefPtr<StyleValue const> Parser::parse_scale_value(TokenStream<ComponentValue>&
return TransformationStyleValue::create(PropertyID::Scale, TransformFunction::Scale3d, { maybe_x.release_nonnull(), maybe_y.release_nonnull(), maybe_z.release_nonnull() });
}
// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand
RefPtr<StyleValue const> Parser::parse_scroll_timeline_value(TokenStream<ComponentValue>& tokens)
{
// [ <'scroll-timeline-name'> <'scroll-timeline-axis'>? ]#
StyleValueVector names;
StyleValueVector axes;
auto transaction = tokens.begin_transaction();
do {
tokens.discard_whitespace();
auto maybe_name = parse_css_value_for_property(PropertyID::ScrollTimelineName, tokens);
if (!maybe_name)
return nullptr;
names.append(maybe_name.release_nonnull());
tokens.discard_whitespace();
if (tokens.next_token().is(Token::Type::Comma)) {
axes.append(KeywordStyleValue::create(Keyword::Block));
tokens.discard_a_token();
// Disallow trailing commas
if (!tokens.has_next_token())
return nullptr;
continue;
}
if (!tokens.has_next_token()) {
axes.append(KeywordStyleValue::create(Keyword::Block));
break;
}
auto maybe_axis = parse_css_value_for_property(PropertyID::ScrollTimelineAxis, tokens);
if (!maybe_axis)
return nullptr;
axes.append(maybe_axis.release_nonnull());
tokens.discard_whitespace();
if (tokens.next_token().is(Token::Type::Comma)) {
tokens.discard_a_token();
// Disallow trailing commas
if (!tokens.has_next_token())
return nullptr;
continue;
}
if (tokens.has_next_token())
return nullptr;
} while (tokens.has_next_token());
transaction.commit();
return ShorthandStyleValue::create(PropertyID::ScrollTimeline,
{ PropertyID::ScrollTimelineName, PropertyID::ScrollTimelineAxis },
{ StyleValueList::create(move(names), StyleValueList::Separator::Comma), StyleValueList::create(move(axes), StyleValueList::Separator::Comma) });
}
// https://drafts.csswg.org/css-scrollbars/#propdef-scrollbar-color
RefPtr<StyleValue const> Parser::parse_scrollbar_color_value(TokenStream<ComponentValue>& tokens)
{

View file

@ -3440,6 +3440,16 @@
"affects-layout": false,
"affects-stacking-context": true
},
"scroll-timeline": {
"affects-layout": false,
"inherited": false,
"initial": "none block",
"multiplicity": "coordinating-list",
"longhands": [
"scroll-timeline-name",
"scroll-timeline-axis"
]
},
"scroll-timeline-axis": {
"affects-layout": false,
"animation-type": "none",

View file

@ -78,7 +78,7 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
return { value.release_nonnull() };
};
auto const coordinating_value_list_shorthand_to_string = [&](StringView entry_when_all_longhands_initial) {
auto const coordinating_value_list_shorthand_to_string = [&](StringView entry_when_all_longhands_initial, Vector<PropertyID> required_longhands = {}) {
auto entry_count = style_value_as_value_list(longhand(m_properties.sub_properties[0])).size();
// If we don't have the same number of values for each longhand, we can't serialize this shorthand.
@ -86,10 +86,15 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
return ""_string;
// We should serialize a longhand if:
// - The longhand is required
// - The value is not the initial value
// - Another longhand value which will be included later in the serialization is valid for this longhand.
auto should_serialize_longhand = [&](size_t entry_index, size_t longhand_index) {
auto longhand_id = m_properties.sub_properties[longhand_index];
if (required_longhands.contains_slow(longhand_id))
return true;
auto longhand_value = style_value_as_value_list(longhand(longhand_id))[entry_index];
if (!longhand_value->equals(style_value_as_value_list(property_initial_value(longhand_id))[0]))
@ -815,6 +820,10 @@ String ShorthandStyleValue::to_string(SerializationMode mode) const
case PropertyID::PlaceItems:
case PropertyID::PlaceSelf:
return positional_value_list_shorthand_to_string(m_properties.values);
case PropertyID::ScrollTimeline:
// NB: We don't need to specify a value to use when the entry is empty as all values are initial since
// scroll-timeline-name is always included
return coordinating_value_list_shorthand_to_string(""sv, { PropertyID::ScrollTimelineName });
case PropertyID::TextDecoration: {
// The rule here seems to be, only print what's different from the default value,
// but if they're all default, print the line.