ソースを参照

LibWeb: Implement counter-[increment,reset,set] properties

These control the state of CSS counters.

Parsing code for `reversed(counter-name)` is implemented, but disabled
for now until we are able to resolve values for those.
Sam Atkins 11 ヶ月 前
コミット
017d6c3314

+ 4 - 1
Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt

@@ -88,6 +88,9 @@ column-count: auto
 column-gap: auto
 content: normal
 content-visibility: visible
+counter-increment: none
+counter-reset: none
+counter-set: none
 cursor: auto
 cx: 0px
 cy: 0px
@@ -120,7 +123,7 @@ grid-row-start: auto
 grid-template-areas: 
 grid-template-columns: 
 grid-template-rows: 
-height: 2074px
+height: 2125px
 image-rendering: auto
 inline-size: auto
 inset-block-end: auto

+ 1 - 0
Userland/Libraries/LibWeb/CMakeLists.txt

@@ -110,6 +110,7 @@ set(SOURCES
     CSS/StyleValues/ColorStyleValue.cpp
     CSS/StyleValues/ConicGradientStyleValue.cpp
     CSS/StyleValues/ContentStyleValue.cpp
+    CSS/StyleValues/CounterDefinitionsStyleValue.cpp
     CSS/StyleValues/DisplayStyleValue.cpp
     CSS/StyleValues/EasingStyleValue.cpp
     CSS/StyleValues/EdgeStyleValue.cpp

+ 14 - 0
Userland/Libraries/LibWeb/CSS/ComputedValues.h

@@ -14,6 +14,7 @@
 #include <LibWeb/CSS/CalculatedOr.h>
 #include <LibWeb/CSS/Clip.h>
 #include <LibWeb/CSS/ColumnCount.h>
+#include <LibWeb/CSS/CountersSet.h>
 #include <LibWeb/CSS/Display.h>
 #include <LibWeb/CSS/GridTrackPlacement.h>
 #include <LibWeb/CSS/GridTrackSize.h>
@@ -315,6 +316,12 @@ struct ContentData {
     String alt_text {};
 };
 
+struct CounterData {
+    FlyString name;
+    bool is_reversed;
+    Optional<CounterValue> value;
+};
+
 struct BorderRadiusData {
     CSS::LengthPercentage horizontal_radius { InitialValues::border_radius() };
     CSS::LengthPercentage vertical_radius { InitialValues::border_radius() };
@@ -632,6 +639,9 @@ protected:
         LengthPercentage y { InitialValues::x() };
 
         CSS::ScrollbarWidth scrollbar_width { InitialValues::scrollbar_width() };
+        Vector<CounterData, 0> counter_increment;
+        Vector<CounterData, 0> counter_reset;
+        Vector<CounterData, 0> counter_set;
     } m_noninherited;
 };
 
@@ -774,6 +784,10 @@ public:
     void set_math_depth(int value) { m_inherited.math_depth = value; }
 
     void set_scrollbar_width(CSS::ScrollbarWidth value) { m_noninherited.scrollbar_width = value; }
+
+    void set_counter_increment(Vector<CounterData> value) { m_noninherited.counter_increment = move(value); }
+    void set_counter_reset(Vector<CounterData> value) { m_noninherited.counter_reset = move(value); }
+    void set_counter_set(Vector<CounterData> value) { m_noninherited.counter_set = move(value); }
 };
 
 }

+ 100 - 0
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -44,6 +44,7 @@
 #include <LibWeb/CSS/StyleValues/BorderRadiusStyleValue.h>
 #include <LibWeb/CSS/StyleValues/ColorStyleValue.h>
 #include <LibWeb/CSS/StyleValues/ContentStyleValue.h>
+#include <LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h>
 #include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
 #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
 #include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
@@ -2891,6 +2892,63 @@ RefPtr<StyleValue> Parser::parse_color_value(TokenStream<ComponentValue>& tokens
     return nullptr;
 }
 
+RefPtr<StyleValue> Parser::parse_counter_definitions_value(TokenStream<ComponentValue>& tokens, AllowReversed allow_reversed, i32 default_value_if_not_reversed)
+{
+    // If AllowReversed is Yes, parses:
+    //   [ <counter-name> <integer>? | <reversed-counter-name> <integer>? ]+
+    // Otherwise parses:
+    //   [ <counter-name> <integer>? ]+
+
+    // FIXME: This disabled parsing of `reversed()` counters. Remove this line once they're supported.
+    allow_reversed = AllowReversed::No;
+
+    auto transaction = tokens.begin_transaction();
+    tokens.skip_whitespace();
+
+    Vector<CounterDefinition> counter_definitions;
+    while (tokens.has_next_token()) {
+        auto per_item_transaction = tokens.begin_transaction();
+        CounterDefinition definition {};
+
+        // <counter-name> | <reversed-counter-name>
+        auto& token = tokens.next_token();
+        if (token.is(Token::Type::Ident)) {
+            definition.name = token.token().ident();
+            definition.is_reversed = false;
+        } else if (allow_reversed == AllowReversed::Yes && token.is_function("reversed"sv)) {
+            TokenStream function_tokens { token.function().values() };
+            function_tokens.skip_whitespace();
+            auto& name_token = function_tokens.next_token();
+            if (!name_token.is(Token::Type::Ident))
+                break;
+            function_tokens.skip_whitespace();
+            if (function_tokens.has_next_token())
+                break;
+
+            definition.name = name_token.token().ident();
+            definition.is_reversed = true;
+        } else {
+            break;
+        }
+        tokens.skip_whitespace();
+
+        // <integer>?
+        definition.value = parse_integer_value(tokens);
+        if (!definition.value && !definition.is_reversed)
+            definition.value = IntegerStyleValue::create(default_value_if_not_reversed);
+
+        counter_definitions.append(move(definition));
+        tokens.skip_whitespace();
+        per_item_transaction.commit();
+    }
+
+    if (counter_definitions.is_empty())
+        return {};
+
+    transaction.commit();
+    return CounterDefinitionsStyleValue::create(move(counter_definitions));
+}
+
 RefPtr<StyleValue> Parser::parse_ratio_value(TokenStream<ComponentValue>& tokens)
 {
     if (auto ratio = parse_ratio(tokens); ratio.has_value())
@@ -4170,6 +4228,36 @@ RefPtr<StyleValue> Parser::parse_content_value(TokenStream<ComponentValue>& toke
     return ContentStyleValue::create(StyleValueList::create(move(content_values), StyleValueList::Separator::Space), move(alt_text));
 }
 
+// https://drafts.csswg.org/css-lists-3/#propdef-counter-increment
+RefPtr<StyleValue> Parser::parse_counter_increment_value(TokenStream<ComponentValue>& tokens)
+{
+    // [ <counter-name> <integer>? ]+ | none
+    if (auto none = parse_all_as_single_none_value(tokens))
+        return none;
+
+    return parse_counter_definitions_value(tokens, AllowReversed::No, 1);
+}
+
+// https://drafts.csswg.org/css-lists-3/#propdef-counter-reset
+RefPtr<StyleValue> Parser::parse_counter_reset_value(TokenStream<ComponentValue>& tokens)
+{
+    // [ <counter-name> <integer>? | <reversed-counter-name> <integer>? ]+ | none
+    if (auto none = parse_all_as_single_none_value(tokens))
+        return none;
+
+    return parse_counter_definitions_value(tokens, AllowReversed::Yes, 0);
+}
+
+// https://drafts.csswg.org/css-lists-3/#propdef-counter-set
+RefPtr<StyleValue> Parser::parse_counter_set_value(TokenStream<ComponentValue>& tokens)
+{
+    // [ <counter-name> <integer>? ]+ | none
+    if (auto none = parse_all_as_single_none_value(tokens))
+        return none;
+
+    return parse_counter_definitions_value(tokens, AllowReversed::No, 0);
+}
+
 // https://www.w3.org/TR/css-display-3/#the-display-properties
 RefPtr<StyleValue> Parser::parse_display_value(TokenStream<ComponentValue>& tokens)
 {
@@ -6758,6 +6846,18 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue>> Parser::parse_css_value(Property
         if (auto parsed_value = parse_content_value(tokens); parsed_value && !tokens.has_next_token())
             return parsed_value.release_nonnull();
         return ParseError::SyntaxError;
+    case PropertyID::CounterIncrement:
+        if (auto parsed_value = parse_counter_increment_value(tokens); parsed_value && !tokens.has_next_token())
+            return parsed_value.release_nonnull();
+        return ParseError::SyntaxError;
+    case PropertyID::CounterReset:
+        if (auto parsed_value = parse_counter_reset_value(tokens); parsed_value && !tokens.has_next_token())
+            return parsed_value.release_nonnull();
+        return ParseError::SyntaxError;
+    case PropertyID::CounterSet:
+        if (auto parsed_value = parse_counter_set_value(tokens); parsed_value && !tokens.has_next_token())
+            return parsed_value.release_nonnull();
+        return ParseError::SyntaxError;
     case PropertyID::Display:
         if (auto parsed_value = parse_display_value(tokens); parsed_value && !tokens.has_next_token())
             return parsed_value.release_nonnull();

+ 8 - 0
Userland/Libraries/LibWeb/CSS/Parser/Parser.h

@@ -288,6 +288,11 @@ private:
     RefPtr<StyleValue> parse_number_or_percentage_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_identifier_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_color_value(TokenStream<ComponentValue>&);
+    enum class AllowReversed {
+        No,
+        Yes,
+    };
+    RefPtr<StyleValue> parse_counter_definitions_value(TokenStream<ComponentValue>&, AllowReversed, i32 default_value_if_not_reversed);
     RefPtr<StyleValue> parse_rect_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_ratio_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_string_value(TokenStream<ComponentValue>&);
@@ -314,6 +319,9 @@ private:
     RefPtr<StyleValue> parse_border_radius_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_border_radius_shorthand_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_content_value(TokenStream<ComponentValue>&);
+    RefPtr<StyleValue> parse_counter_increment_value(TokenStream<ComponentValue>&);
+    RefPtr<StyleValue> parse_counter_reset_value(TokenStream<ComponentValue>&);
+    RefPtr<StyleValue> parse_counter_set_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_display_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_flex_value(TokenStream<ComponentValue>&);
     RefPtr<StyleValue> parse_flex_flow_value(TokenStream<ComponentValue>&);

+ 36 - 0
Userland/Libraries/LibWeb/CSS/Properties.json

@@ -986,6 +986,42 @@
       "content-visibility"
     ]
   },
+  "counter-increment": {
+    "animation-type": "by-computed-value",
+    "inherited": false,
+    "initial": "none",
+    "valid-types": [
+      "custom-ident",
+      "integer [-∞,∞]"
+    ],
+    "valid-identifiers": [
+      "none"
+    ]
+  },
+  "counter-reset": {
+    "animation-type": "by-computed-value",
+    "inherited": false,
+    "initial": "none",
+    "valid-types": [
+      "custom-ident",
+      "integer [-∞,∞]"
+    ],
+    "valid-identifiers": [
+      "none"
+    ]
+  },
+  "counter-set": {
+    "animation-type": "by-computed-value",
+    "inherited": false,
+    "initial": "none",
+    "valid-types": [
+      "custom-ident",
+      "integer [-∞,∞]"
+    ],
+    "valid-identifiers": [
+      "none"
+    ]
+  },
   "cursor": {
     "affects-layout": false,
     "animation-type": "discrete",

+ 37 - 0
Userland/Libraries/LibWeb/CSS/StyleProperties.cpp

@@ -11,6 +11,7 @@
 #include <LibWeb/CSS/StyleProperties.h>
 #include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
 #include <LibWeb/CSS/StyleValues/ContentStyleValue.h>
+#include <LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h>
 #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
 #include <LibWeb/CSS/StyleValues/GridAutoFlowStyleValue.h>
 #include <LibWeb/CSS/StyleValues/GridTemplateAreaStyleValue.h>
@@ -1093,6 +1094,42 @@ QuotesData StyleProperties::quotes() const
     return InitialValues::quotes();
 }
 
+Vector<CounterData> StyleProperties::counter_data(PropertyID property_id) const
+{
+    auto value = property(property_id);
+
+    if (value->is_counter_definitions()) {
+        auto& counter_definitions = value->as_counter_definitions().counter_definitions();
+        Vector<CounterData> result;
+        for (auto& counter : counter_definitions) {
+            CounterData data {
+                .name = counter.name,
+                .is_reversed = counter.is_reversed,
+                .value = {},
+            };
+            if (counter.value) {
+                if (counter.value->is_integer()) {
+                    data.value = AK::clamp_to<i32>(counter.value->as_integer().integer());
+                } else if (counter.value->is_calculated()) {
+                    auto maybe_int = counter.value->as_calculated().resolve_integer();
+                    if (maybe_int.has_value())
+                        data.value = AK::clamp_to<i32>(*maybe_int);
+                } else {
+                    dbgln("Unimplemented type for {} integer value: '{}'", string_from_property_id(property_id), counter.value->to_string());
+                }
+            }
+            result.append(move(data));
+        }
+        return result;
+    }
+
+    if (value->to_identifier() == ValueID::None)
+        return {};
+
+    dbgln("Unhandled type for {} value: '{}'", string_from_property_id(property_id), value->to_string());
+    return {};
+}
+
 Optional<CSS::ScrollbarWidth> StyleProperties::scrollbar_width() const
 {
     auto value = property(CSS::PropertyID::ScrollbarWidth);

+ 1 - 0
Userland/Libraries/LibWeb/CSS/StyleProperties.h

@@ -177,6 +177,7 @@ public:
     int math_depth() const { return m_math_depth; }
 
     QuotesData quotes() const;
+    Vector<CounterData> counter_data(PropertyID) const;
 
     Optional<CSS::ScrollbarWidth> scrollbar_width() const;
 

+ 1 - 0
Userland/Libraries/LibWeb/CSS/StyleValue.cpp

@@ -20,6 +20,7 @@
 #include <LibWeb/CSS/StyleValues/ColorStyleValue.h>
 #include <LibWeb/CSS/StyleValues/ConicGradientStyleValue.h>
 #include <LibWeb/CSS/StyleValues/ContentStyleValue.h>
+#include <LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h>
 #include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
 #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
 #include <LibWeb/CSS/StyleValues/EasingStyleValue.h>

+ 1 - 0
Userland/Libraries/LibWeb/CSS/StyleValue.h

@@ -92,6 +92,7 @@ using StyleValueVector = Vector<ValueComparingNonnullRefPtr<StyleValue const>>;
     __ENUMERATE_STYLE_VALUE_TYPE(Color, color)                             \
     __ENUMERATE_STYLE_VALUE_TYPE(ConicGradient, conic_gradient)            \
     __ENUMERATE_STYLE_VALUE_TYPE(Content, content)                         \
+    __ENUMERATE_STYLE_VALUE_TYPE(CounterDefinitions, counter_definitions)  \
     __ENUMERATE_STYLE_VALUE_TYPE(CustomIdent, custom_ident)                \
     __ENUMERATE_STYLE_VALUE_TYPE(Display, display)                         \
     __ENUMERATE_STYLE_VALUE_TYPE(Easing, easing)                           \

+ 46 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.cpp

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <atkinssj@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CounterDefinitionsStyleValue.h"
+#include <LibWeb/CSS/Serialize.h>
+
+namespace Web::CSS {
+
+String CounterDefinitionsStyleValue::to_string() const
+{
+    StringBuilder stb;
+
+    for (auto const& counter_definition : m_counter_definitions) {
+        if (!stb.is_empty())
+            stb.append(' ');
+
+        if (counter_definition.is_reversed)
+            stb.appendff("reversed({})", counter_definition.name);
+        else
+            stb.append(counter_definition.name);
+
+        if (counter_definition.value)
+            stb.appendff(" {}", counter_definition.value->to_string());
+    }
+
+    return stb.to_string_without_validation();
+}
+
+bool CounterDefinitionsStyleValue::properties_equal(CounterDefinitionsStyleValue const& other) const
+{
+    if (m_counter_definitions.size() != other.counter_definitions().size())
+        return false;
+
+    for (auto i = 0u; i < m_counter_definitions.size(); i++) {
+        auto const& ours = m_counter_definitions[i];
+        auto const& theirs = other.counter_definitions()[i];
+        if (ours.name != theirs.name || ours.is_reversed != theirs.is_reversed || ours.value != theirs.value)
+            return false;
+    }
+    return true;
+}
+
+}

+ 47 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <atkinssj@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/FlyString.h>
+#include <LibWeb/CSS/StyleValue.h>
+
+namespace Web::CSS {
+
+struct CounterDefinition {
+    FlyString name;
+    bool is_reversed;
+    ValueComparingRefPtr<StyleValue const> value;
+};
+
+/**
+ * Holds a list of CounterDefinitions.
+ * Shared between counter-increment, counter-reset, and counter-set properties that have (almost) identical grammar.
+ */
+class CounterDefinitionsStyleValue : public StyleValueWithDefaultOperators<CounterDefinitionsStyleValue> {
+public:
+    static ValueComparingNonnullRefPtr<CounterDefinitionsStyleValue> create(Vector<CounterDefinition> counter_definitions)
+    {
+        return adopt_ref(*new (nothrow) CounterDefinitionsStyleValue(move(counter_definitions)));
+    }
+    virtual ~CounterDefinitionsStyleValue() override = default;
+
+    auto const& counter_definitions() const { return m_counter_definitions; }
+    virtual String to_string() const override;
+
+    bool properties_equal(CounterDefinitionsStyleValue const& other) const;
+
+private:
+    explicit CounterDefinitionsStyleValue(Vector<CounterDefinition> counter_definitions)
+        : StyleValueWithDefaultOperators(Type::CounterDefinitions)
+        , m_counter_definitions(move(counter_definitions))
+    {
+    }
+
+    Vector<CounterDefinition> m_counter_definitions;
+};
+
+}

+ 15 - 4
Userland/Libraries/LibWeb/DOM/Element.cpp

@@ -2711,16 +2711,27 @@ CSS::CountersSet& Element::ensure_counters_set()
 }
 
 // https://drafts.csswg.org/css-lists-3/#auto-numbering
-void Element::resolve_counters(CSS::StyleProperties&)
+void Element::resolve_counters(CSS::StyleProperties& style)
 {
     // Resolving counter values on a given element is a multi-step process:
 
     // 1. Existing counters are inherited from previous elements.
     inherit_counters();
 
-    // TODO: 2. New counters are instantiated (counter-reset).
-    // TODO: 3. Counter values are incremented (counter-increment).
-    // TODO: 4. Counter values are explicitly set (counter-set).
+    // 2. New counters are instantiated (counter-reset).
+    auto counter_reset = style.counter_data(CSS::PropertyID::CounterReset);
+    for (auto const& counter : counter_reset)
+        ensure_counters_set().instantiate_a_counter(counter.name, unique_id(), counter.is_reversed, counter.value);
+
+    // 3. Counter values are incremented (counter-increment).
+    auto counter_increment = style.counter_data(CSS::PropertyID::CounterIncrement);
+    for (auto const& counter : counter_increment)
+        ensure_counters_set().increment_a_counter(counter.name, unique_id(), *counter.value);
+
+    // 4. Counter values are explicitly set (counter-set).
+    auto counter_set = style.counter_data(CSS::PropertyID::CounterSet);
+    for (auto const& counter : counter_set)
+        ensure_counters_set().set_a_counter(counter.name, unique_id(), *counter.value);
 
     // 5. Counter values are used (counter()/counters()).
     // NOTE: This happens when we process the `content` property.

+ 1 - 0
Userland/Libraries/LibWeb/Forward.h

@@ -120,6 +120,7 @@ class Clip;
 class ColorStyleValue;
 class ConicGradientStyleValue;
 class ContentStyleValue;
+class CounterDefinitionsStyleValue;
 class CustomIdentStyleValue;
 class Display;
 class DisplayStyleValue;

+ 3 - 0
Userland/Libraries/LibWeb/Layout/Node.cpp

@@ -860,6 +860,9 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style)
 
     computed_values.set_math_depth(computed_style.math_depth());
     computed_values.set_quotes(computed_style.quotes());
+    computed_values.set_counter_increment(computed_style.counter_data(CSS::PropertyID::CounterIncrement));
+    computed_values.set_counter_reset(computed_style.counter_data(CSS::PropertyID::CounterReset));
+    computed_values.set_counter_set(computed_style.counter_data(CSS::PropertyID::CounterSet));
 
     if (auto object_fit = computed_style.object_fit(); object_fit.has_value())
         computed_values.set_object_fit(object_fit.value());