LibWeb: Support top-level tree counting functions

Adds support for `sibling-index()` and `sibling-count()` when parsing
`<number>` and `<integer>`. This is achieved by a new
`TreeCountingFunctionStyleValue` class which is converted within
`absolutized` to `NumberStyleValue` and `IntegerStyleValue` respectively

There are still a few kinks to work out in order to support these
everywhere, namely:
 - There are some `StyleValue`s which aren't absolutized (i.e. those
   which are stored within another `StyleValue` without an
   `absolutize()` method.
 - We don't have a way to represent this new `StyleValue` within
   `{Number,Integer}OrCalculated`. This would be fixed if we were to
   instead just use the `StyleValue` classes until style computation at
   which time they would be absolutized into their respective
   primitives (double, i64, etc) bypassing the need for *OrCalculated
   entirely.
This commit is contained in:
Callum Law 2025-10-01 20:02:33 +13:00 committed by Tim Ledbetter
parent 9cd23e3ae5
commit 831e471444
17 changed files with 184 additions and 32 deletions

View File

@ -275,6 +275,7 @@ set(SOURCES
CSS/StyleValues/TextUnderlinePositionStyleValue.cpp
CSS/StyleValues/TransformationStyleValue.cpp
CSS/StyleValues/TransitionStyleValue.cpp
CSS/StyleValues/TreeCountingFunctionStyleValue.cpp
CSS/StyleValues/UnicodeRangeStyleValue.cpp
CSS/StyleValues/UnresolvedStyleValue.cpp
CSS/Supports.cpp

View File

@ -34,6 +34,7 @@
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
#include <LibWeb/CSS/StyleValues/ShadowStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
#include <LibWeb/CSS/StyleValues/TreeCountingFunctionStyleValue.h>
#include <LibWeb/CSS/Supports.h>
#include <LibWeb/CSS/URL.h>
#include <LibWeb/Forward.h>
@ -169,6 +170,7 @@ public:
NonnullRefPtr<StyleValue const> parse_with_a_syntax(Vector<ComponentValue> const& input, SyntaxNode const& syntax, Optional<DOM::AbstractElement> const& element = {});
RefPtr<CalculatedStyleValue const> parse_calculated_value(ComponentValue const&);
RefPtr<TreeCountingFunctionStyleValue const> parse_tree_counting_function(TokenStream<ComponentValue>&, TreeCountingFunctionStyleValue::ComputedType);
private:
Parser(ParsingParams const&, Vector<Token>);

View File

@ -190,11 +190,7 @@ Optional<Parser::PropertyAndValue> Parser::parse_css_value_for_properties(Readon
auto context_guard = push_temporary_value_parsing_context(*property);
auto transaction = tokens.begin_transaction();
if (auto value = parse_integer_value(tokens)) {
if (value->is_calculated()) {
transaction.commit();
return PropertyAndValue { *property, value };
}
if (value->is_integer() && property_accepts_integer(*property, value->as_integer().integer())) {
if ((value->is_integer() && property_accepts_integer(*property, value->as_integer().integer())) || !value->is_integer()) {
transaction.commit();
return PropertyAndValue { *property, value };
}
@ -205,11 +201,7 @@ Optional<Parser::PropertyAndValue> Parser::parse_css_value_for_properties(Readon
auto context_guard = push_temporary_value_parsing_context(*property);
auto transaction = tokens.begin_transaction();
if (auto value = parse_number_value(tokens)) {
if (value->is_calculated()) {
transaction.commit();
return PropertyAndValue { *property, value };
}
if (value->is_number() && property_accepts_number(*property, value->as_number().number())) {
if ((value->is_number() && property_accepts_number(*property, value->as_number().number())) || !value->is_number()) {
transaction.commit();
return PropertyAndValue { *property, value };
}

View File

@ -256,6 +256,9 @@ Optional<FrequencyPercentage> Parser::parse_frequency_percentage(TokenStream<Com
Optional<IntegerOrCalculated> Parser::parse_integer(TokenStream<ComponentValue>& tokens)
{
// FIXME: We don't have a way to represent tree counting functions within IntegerOrCalculated, we should avoid
// parsing directly to IntegerOrCalculated unless tree counting functions are disallowed in the relevant
// context
if (auto value = parse_integer_value(tokens)) {
if (value->is_integer())
return value->as_integer().integer();
@ -293,6 +296,9 @@ Optional<LengthPercentage> Parser::parse_length_percentage(TokenStream<Component
Optional<NumberOrCalculated> Parser::parse_number(TokenStream<ComponentValue>& tokens)
{
// FIXME: We don't have a way to represent tree counting functions within NumberOrCalculated, we should avoid
// parsing directly to NumberOrCalculated unless tree counting functions are disallowed in the relevant
// context
if (auto value = parse_number_value(tokens)) {
if (value->is_number())
return value->as_number().number();
@ -786,6 +792,9 @@ RefPtr<StyleValue const> Parser::parse_integer_value(TokenStream<ComponentValue>
return calc;
}
if (auto tree_counting_function = parse_tree_counting_function(tokens, TreeCountingFunctionStyleValue::ComputedType::Integer); tree_counting_function)
return tree_counting_function;
return nullptr;
}
@ -802,6 +811,9 @@ RefPtr<StyleValue const> Parser::parse_number_value(TokenStream<ComponentValue>&
return calc;
}
if (auto tree_counting_function = parse_tree_counting_function(tokens, TreeCountingFunctionStyleValue::ComputedType::Number); tree_counting_function)
return tree_counting_function;
return nullptr;
}
@ -4455,6 +4467,33 @@ RefPtr<CalculationNode const> Parser::parse_a_calculation(Vector<ComponentValue>
return simplify_a_calculation_tree(*calculation_tree, context, CalculationResolutionContext {});
}
// https://drafts.csswg.org/css-values-5/#tree-counting
RefPtr<TreeCountingFunctionStyleValue const> Parser::parse_tree_counting_function(TokenStream<ComponentValue>& tokens, TreeCountingFunctionStyleValue::ComputedType computed_type)
{
if (!context_allows_tree_counting_functions())
return nullptr;
auto has_no_arguments = [](Vector<ComponentValue> const& component_values) {
return !any_of(component_values, [](ComponentValue const& value) { return !value.is(Token::Type::Whitespace); });
};
auto transaction = tokens.begin_transaction();
auto token = tokens.consume_a_token();
if (token.is_function("sibling-count"sv) && has_no_arguments(token.function().value)) {
transaction.commit();
return TreeCountingFunctionStyleValue::create(TreeCountingFunctionStyleValue::TreeCountingFunction::SiblingCount, computed_type);
}
if (token.is_function("sibling-index"sv) && has_no_arguments(token.function().value)) {
transaction.commit();
return TreeCountingFunctionStyleValue::create(TreeCountingFunctionStyleValue::TreeCountingFunction::SiblingIndex, computed_type);
}
return nullptr;
}
// https://drafts.csswg.org/css-color-4/#typedef-opacity-opacity-value
RefPtr<StyleValue const> Parser::parse_opacity_value(TokenStream<ComponentValue>& tokens)
{

View File

@ -71,6 +71,7 @@
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransitionStyleValue.h>
#include <LibWeb/CSS/StyleValues/TreeCountingFunctionStyleValue.h>
#include <LibWeb/CSS/StyleValues/URLStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnicodeRangeStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>

View File

@ -87,6 +87,7 @@ namespace Web::CSS {
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Time, time, TimeStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Transformation, transformation, TransformationStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Transition, transition, TransitionStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(TreeCountingFunction, tree_counting_function, TreeCountingFunctionStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(UnicodeRange, unicode_range, UnicodeRangeStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(Unresolved, unresolved, UnresolvedStyleValue) \
__ENUMERATE_CSS_STYLE_VALUE_TYPE(URL, url, URLStyleValue) \

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "TreeCountingFunctionStyleValue.h"
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
namespace Web::CSS {
String TreeCountingFunctionStyleValue::to_string(SerializationMode) const
{
switch (m_function) {
case TreeCountingFunction::SiblingCount:
return "sibling-count()"_string;
case TreeCountingFunction::SiblingIndex:
return "sibling-index()"_string;
}
VERIFY_NOT_REACHED();
}
size_t TreeCountingFunctionStyleValue::resolve(TreeCountingFunctionResolutionContext const& tree_counting_function_resolution_context) const
{
switch (m_function) {
case TreeCountingFunction::SiblingCount:
return tree_counting_function_resolution_context.sibling_count;
case TreeCountingFunction::SiblingIndex:
return tree_counting_function_resolution_context.sibling_index;
}
VERIFY_NOT_REACHED();
}
ValueComparingNonnullRefPtr<StyleValue const> TreeCountingFunctionStyleValue::absolutized(ComputationContext const& computation_context) const
{
// FIXME: We should clamp this value in case it falls outside the valid range for the context it is in
VERIFY(computation_context.tree_counting_function_resolution_context.has_value());
size_t value = resolve(computation_context.tree_counting_function_resolution_context.value());
switch (m_computed_type) {
case ComputedType::Integer:
return IntegerStyleValue::create(value);
case ComputedType::Number:
return NumberStyleValue::create(static_cast<double>(value));
}
VERIFY_NOT_REACHED();
}
bool TreeCountingFunctionStyleValue::equals(StyleValue const& other) const
{
if (type() != other.type())
return false;
auto const& other_tree_counting_function = other.as_tree_counting_function();
return m_function == other_tree_counting_function.m_function && m_computed_type == other_tree_counting_function.m_computed_type;
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2025, Callum Law <callumlaw1709@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/CSS/StyleValues/StyleValue.h>
namespace Web::CSS {
class TreeCountingFunctionStyleValue final : public StyleValue {
public:
enum class TreeCountingFunction : u8 {
SiblingCount,
SiblingIndex
};
enum class ComputedType : u8 {
Number,
Integer
};
static ValueComparingNonnullRefPtr<TreeCountingFunctionStyleValue const> create(TreeCountingFunction function, ComputedType computed_type)
{
return adopt_ref(*new (nothrow) TreeCountingFunctionStyleValue(function, computed_type));
}
virtual ~TreeCountingFunctionStyleValue() override = default;
virtual String to_string(SerializationMode) const override;
size_t resolve(TreeCountingFunctionResolutionContext const&) const;
virtual ValueComparingNonnullRefPtr<StyleValue const> absolutized(ComputationContext const&) const override;
virtual bool equals(StyleValue const& other) const override;
private:
TreeCountingFunctionStyleValue(TreeCountingFunction function, ComputedType computed_type)
: StyleValue(Type::TreeCountingFunction)
, m_function(function)
, m_computed_type(computed_type)
{
}
TreeCountingFunction m_function;
ComputedType m_computed_type;
};
}

View File

@ -385,6 +385,7 @@ class TimeStyleValue;
class Transformation;
class TransformationStyleValue;
class TransitionStyleValue;
class TreeCountingFunctionStyleValue;
class UnicodeRangeStyleValue;
class UnresolvedStyleValue;
class URL;

View File

@ -2,8 +2,7 @@ Harness status: OK
Found 22 tests
21 Pass
1 Fail
22 Pass
Pass e.style['animation-timing-function'] = "linear" should set the property value
Pass e.style['animation-timing-function'] = "ease" should set the property value
Pass e.style['animation-timing-function'] = "ease-in" should set the property value
@ -13,7 +12,7 @@ Pass e.style['animation-timing-function'] = "cubic-bezier(0.1, 0.2, 0.8, 0.9)" s
Pass e.style['animation-timing-function'] = "cubic-bezier(0, -2, 1, 3)" should set the property value
Pass e.style['animation-timing-function'] = "cubic-bezier(0, 0.7, 1, 1.3)" should set the property value
Pass e.style['animation-timing-function'] = "cubic-bezier(calc(-2), calc(0.7 / 2), calc(1.5), calc(0))" should set the property value
Fail e.style['animation-timing-function'] = "cubic-bezier(0, sibling-index(), 1, sign(2em - 20px))" should set the property value
Pass e.style['animation-timing-function'] = "cubic-bezier(0, sibling-index(), 1, sign(2em - 20px))" should set the property value
Pass e.style['animation-timing-function'] = "steps(4, start)" should set the property value
Pass e.style['animation-timing-function'] = "steps(2, end)" should set the property value
Pass e.style['animation-timing-function'] = "steps( 2, end )" should set the property value

View File

@ -2,13 +2,12 @@ Harness status: OK
Found 8 tests
6 Pass
2 Fail
8 Pass
Pass e.style['font-variation-settings'] = "\"wght\" sign(1em - 1px)" should set the property value
Fail e.style['font-variation-settings'] = "\"wght\" sibling-index()" should set the property value
Pass e.style['font-variation-settings'] = "\"wght\" sibling-index()" should set the property value
Pass e.style['font-variation-settings'] = "\"wght\" calc(10)" should set the property value
Pass e.style['font-variation-settings'] = "\"wght\" sign(2px)" should set the property value
Pass Property font-variation-settings value '"wght" sign(1em - 1px)'
Fail Property font-variation-settings value '"wght" sibling-index()'
Pass Property font-variation-settings value '"wght" sibling-index()'
Pass Property font-variation-settings value '"wght" calc(10)'
Pass Property font-variation-settings value '"wght" sign(2px)'

View File

@ -2,8 +2,7 @@ Harness status: OK
Found 22 tests
21 Pass
1 Fail
22 Pass
Pass e.style['transition-timing-function'] = "linear" should set the property value
Pass e.style['transition-timing-function'] = "linear(0 0%, 0.5 50%, 1 100%)" should set the property value
Pass e.style['transition-timing-function'] = "linear(0 0%, 10 10%, 10 50%, 25.4 75%, 100 100%)" should set the property value
@ -24,5 +23,5 @@ Pass e.style['transition-timing-function'] = "steps(2, jump-start)" should set t
Pass e.style['transition-timing-function'] = "steps(2, jump-end)" should set the property value
Pass e.style['transition-timing-function'] = "steps(2, jump-both)" should set the property value
Pass e.style['transition-timing-function'] = "steps(2, jump-none)" should set the property value
Fail e.style['transition-timing-function'] = "steps(sibling-index(), jump-none)" should set the property value
Pass e.style['transition-timing-function'] = "steps(sibling-index(), jump-none)" should set the property value
Pass e.style['transition-timing-function'] = "linear, ease, linear" should set the property value

View File

@ -2,5 +2,5 @@ Harness status: OK
Found 1 tests
1 Fail
Fail Host children have sibling-index() and sibling-count() based on the DOM tree order
1 Pass
Pass Host children have sibling-index() and sibling-count() based on the DOM tree order

View File

@ -2,15 +2,15 @@ Harness status: OK
Found 10 tests
4 Pass
6 Fail
6 Pass
4 Fail
Fail e.style['left'] = "calc(1px * sibling-index())" should set the property value
Fail e.style['left'] = "calc(1px * sibling-index( ))" should set the property value
Fail e.style['z-index'] = "sibling-index()" should set the property value
Pass e.style['z-index'] = "sibling-index()" should set the property value
Pass e.style['left'] = "calc(1px * sibling-index(100px))" should not set the property value
Pass e.style['left'] = "calc(1px * sibling-index(1))" should not set the property value
Fail e.style['left'] = "calc(1px * sibling-count())" should set the property value
Fail e.style['left'] = "calc(1px * sibling-count( ))" should set the property value
Fail e.style['z-index'] = "sibling-count()" should set the property value
Pass e.style['z-index'] = "sibling-count()" should set the property value
Pass e.style['left'] = "calc(1px * sibling-count(100px))" should not set the property value
Pass e.style['left'] = "calc(1px * sibling-count(1))" should not set the property value

View File

@ -2,6 +2,7 @@ Harness status: OK
Found 2 tests
2 Fail
Fail Initially, the sibling-index() is 3 for #target
1 Pass
1 Fail
Pass Initially, the sibling-index() is 3 for #target
Fail Removing a preceding sibling of #target reduces the sibling-index()

View File

@ -2,6 +2,7 @@ Harness status: OK
Found 2 tests
2 Fail
Fail Initially, the sibling-index() is 3 for #target
1 Pass
1 Fail
Pass Initially, the sibling-index() is 3 for #target
Fail Removing a preceding sibling of #target reduces the sibling-index()

View File

@ -2,6 +2,7 @@ Harness status: OK
Found 2 tests
2 Fail
1 Pass
1 Fail
Fail sibling-index() and sibling-count() evaluates to 0 from outer tree with ::part
Fail sibling-index() and sibling-count() evaluate as normal from inner tree
Pass sibling-index() and sibling-count() evaluate as normal from inner tree