Kaynağa Gözat

LibWeb: Give DOM Elements a CountersSet

This represents each element's set of CSS counters.
https://drafts.csswg.org/css-lists-3/#css-counters-set

Counters are resolved while building the tree. Most elements will not
have any counters to keep track of, so as an optimization, we don't
create a CountersSet object until the element actually needs one.

In order to properly support counters on pseudo-elements, the
CountersSet needs to go somewhere else. However, my experiments with
placing it on the Layout::Node kept hitting a wall. For now, this is
fairly simple at least.
Sam Atkins 1 yıl önce
ebeveyn
işleme
708f49d906

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

@@ -33,6 +33,7 @@ set(SOURCES
     CSS/AnimationEvent.cpp
     CSS/CalculatedOr.cpp
     CSS/Clip.cpp
+    CSS/CountersSet.cpp
     CSS/CSS.cpp
     CSS/CSSAnimation.cpp
     CSS/CSSConditionRule.cpp

+ 103 - 0
Userland/Libraries/LibWeb/CSS/CountersSet.cpp

@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CountersSet.h"
+#include <LibWeb/DOM/Element.h>
+#include <LibWeb/DOM/Node.h>
+
+namespace Web::CSS {
+
+// https://drafts.csswg.org/css-lists-3/#instantiate-counter
+Counter& CountersSet::instantiate_a_counter(FlyString name, i32 originating_element_id, bool reversed, Optional<CounterValue> value)
+{
+    // 1. Let counters be element’s CSS counters set.
+    auto* element = DOM::Node::from_unique_id(originating_element_id);
+
+    // 2. Let innermost counter be the last counter in counters with the name name.
+    //    If innermost counter’s originating element is element or a previous sibling of element,
+    //    remove innermost counter from counters.
+    auto innermost_counter = last_counter_with_name(name);
+    if (innermost_counter.has_value()) {
+        auto* originating_node = DOM::Node::from_unique_id(innermost_counter->originating_element_id);
+        VERIFY(originating_node);
+        auto& innermost_element = verify_cast<DOM::Element>(*originating_node);
+
+        if (&innermost_element == element
+            || (innermost_element.parent() == element->parent() && innermost_element.is_before(*element))) {
+
+            m_counters.remove_first_matching([&innermost_counter](auto& it) {
+                return it.name == innermost_counter->name
+                    && it.originating_element_id == innermost_counter->originating_element_id;
+            });
+        }
+    }
+
+    // 3. Append a new counter to counters with name name, originating element element,
+    //    reversed being reversed, and initial value value (if given)
+    m_counters.append({
+        .name = move(name),
+        .originating_element_id = originating_element_id,
+        .reversed = reversed,
+        .value = value,
+    });
+
+    return m_counters.last();
+}
+
+// https://drafts.csswg.org/css-lists-3/#propdef-counter-set
+void CountersSet::set_a_counter(FlyString name, i32 originating_element_id, CounterValue value)
+{
+    if (auto existing_counter = last_counter_with_name(name); existing_counter.has_value()) {
+        existing_counter->value = value;
+        return;
+    }
+
+    // If there is not currently a counter of the given name on the element, the element instantiates
+    // a new counter of the given name with a starting value of 0 before setting or incrementing its value.
+    // https://drafts.csswg.org/css-lists-3/#valdef-counter-set-counter-name-integer
+    auto& counter = instantiate_a_counter(name, originating_element_id, false, 0);
+    counter.value = value;
+}
+
+// https://drafts.csswg.org/css-lists-3/#propdef-counter-increment
+void CountersSet::increment_a_counter(FlyString name, i32 originating_element_id, CounterValue amount)
+{
+    if (auto existing_counter = last_counter_with_name(name); existing_counter.has_value()) {
+        // FIXME: How should we handle existing counters with no value? Can that happen?
+        VERIFY(existing_counter->value.has_value());
+        existing_counter->value->saturating_add(amount.value());
+        return;
+    }
+
+    // If there is not currently a counter of the given name on the element, the element instantiates
+    // a new counter of the given name with a starting value of 0 before setting or incrementing its value.
+    // https://drafts.csswg.org/css-lists-3/#valdef-counter-set-counter-name-integer
+    auto& counter = instantiate_a_counter(name, originating_element_id, false, 0);
+    counter.value->saturating_add(amount.value());
+}
+
+Optional<Counter&> CountersSet::last_counter_with_name(FlyString const& name)
+{
+    for (auto& counter : m_counters.in_reverse()) {
+        if (counter.name == name)
+            return counter;
+    }
+    return {};
+}
+
+Optional<Counter&> CountersSet::counter_with_same_name_and_creator(FlyString const& name, i32 originating_element_id)
+{
+    return m_counters.first_matching([&](auto& it) {
+        return it.name == name && it.originating_element_id == originating_element_id;
+    });
+}
+
+void CountersSet::append_copy(Counter const& counter)
+{
+    m_counters.append(counter);
+}
+
+}

+ 50 - 0
Userland/Libraries/LibWeb/CSS/CountersSet.h

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Checked.h>
+#include <AK/FlyString.h>
+#include <AK/Optional.h>
+
+namespace Web::CSS {
+
+// "UAs may have implementation-specific limits on the maximum or minimum value of a counter.
+// If a counter reset, set, or increment would push the value outside of that range, the value
+// must be clamped to that range." - https://drafts.csswg.org/css-lists-3/#auto-numbering
+// So, we use a Checked<i32> and saturating addition/subtraction.
+using CounterValue = Checked<i32>;
+
+// https://drafts.csswg.org/css-lists-3/#counter
+struct Counter {
+    FlyString name;
+    i32 originating_element_id; // "creator"
+    bool reversed { false };
+    Optional<CounterValue> value;
+};
+
+// https://drafts.csswg.org/css-lists-3/#css-counters-set
+class CountersSet {
+public:
+    CountersSet() = default;
+    ~CountersSet() = default;
+
+    Counter& instantiate_a_counter(FlyString name, i32 originating_element_id, bool reversed, Optional<CounterValue>);
+    void set_a_counter(FlyString name, i32 originating_element_id, CounterValue value);
+    void increment_a_counter(FlyString name, i32 originating_element_id, CounterValue amount);
+    void append_copy(Counter const&);
+
+    Optional<Counter&> last_counter_with_name(FlyString const& name);
+    Optional<Counter&> counter_with_same_name_and_creator(FlyString const& name, i32 originating_element_id);
+
+    Vector<Counter> const& counters() const { return m_counters; }
+    bool is_empty() const { return m_counters.is_empty(); }
+
+private:
+    Vector<Counter> m_counters;
+};
+
+}

+ 88 - 0
Userland/Libraries/LibWeb/DOM/Element.cpp

@@ -2696,4 +2696,92 @@ WebIDL::ExceptionOr<void> Element::set_html_unsafe(StringView html)
     return {};
 }
 
+Optional<CSS::CountersSet const&> Element::counters_set()
+{
+    if (!m_counters_set)
+        return {};
+    return *m_counters_set;
+}
+
+CSS::CountersSet& Element::ensure_counters_set()
+{
+    if (!m_counters_set)
+        m_counters_set = make<CSS::CountersSet>();
+    return *m_counters_set;
+}
+
+// https://drafts.csswg.org/css-lists-3/#auto-numbering
+void Element::resolve_counters(CSS::StyleProperties&)
+{
+    // 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).
+
+    // 5. Counter values are used (counter()/counters()).
+    // NOTE: This happens when we process the `content` property.
+}
+
+// https://drafts.csswg.org/css-lists-3/#inherit-counters
+void Element::inherit_counters()
+{
+    // 1. If element is the root of its document tree, the element has an initially-empty CSS counters set.
+    //    Return.
+    auto* parent = parent_element();
+    if (parent == nullptr) {
+        // NOTE: We represent an empty counters set with `m_counters_set = nullptr`.
+        m_counters_set = nullptr;
+        return;
+    }
+
+    // 2. Let element counters, representing element’s own CSS counters set, be a copy of the CSS counters
+    //    set of element’s parent element.
+    OwnPtr<CSS::CountersSet> element_counters;
+    // OPTIMIZATION: If parent has a set, we create a copy. Otherwise, we avoid allocating one until we need
+    // to add something to it.
+    auto ensure_element_counters = [&]() {
+        if (!element_counters)
+            element_counters = make<CSS::CountersSet>();
+    };
+    if (parent->has_non_empty_counters_set()) {
+        element_counters = make<CSS::CountersSet>();
+        *element_counters = *parent_element()->counters_set();
+    }
+
+    // 3. Let sibling counters be the CSS counters set of element’s preceding sibling (if it has one),
+    //    or an empty CSS counters set otherwise.
+    //    For each counter of sibling counters, if element counters does not already contain a counter with
+    //    the same name, append a copy of counter to element counters.
+    if (auto* const sibling = previous_sibling_of_type<Element>(); sibling && sibling->has_non_empty_counters_set()) {
+        auto& sibling_counters = sibling->counters_set().release_value();
+        ensure_element_counters();
+        for (auto const& counter : sibling_counters.counters()) {
+            if (!element_counters->last_counter_with_name(counter.name).has_value())
+                element_counters->append_copy(counter);
+        }
+    }
+
+    // 4. Let value source be the CSS counters set of the element immediately preceding element in tree order.
+    //    For each source counter of value source, if element counters contains a counter with the same name
+    //    and creator, then set the value of that counter to source counter’s value.
+    if (auto* const previous = previous_element_in_pre_order(); previous && previous->has_non_empty_counters_set()) {
+        // NOTE: If element_counters is empty (AKA null) then we can skip this since nothing will match.
+        if (element_counters) {
+            auto& value_source = previous->counters_set().release_value();
+            for (auto const& source_counter : value_source.counters()) {
+                auto maybe_existing_counter = element_counters->counter_with_same_name_and_creator(source_counter.name, source_counter.originating_element_id);
+                if (maybe_existing_counter.has_value())
+                    maybe_existing_counter->value = source_counter.value;
+            }
+        }
+    }
+
+    VERIFY(!element_counters || !element_counters->is_empty());
+    m_counters_set = move(element_counters);
+}
+
 }

+ 9 - 0
Userland/Libraries/LibWeb/DOM/Element.h

@@ -11,6 +11,7 @@
 #include <LibWeb/Bindings/ElementPrototype.h>
 #include <LibWeb/Bindings/Intrinsics.h>
 #include <LibWeb/Bindings/ShadowRootPrototype.h>
+#include <LibWeb/CSS/CountersSet.h>
 #include <LibWeb/CSS/Selector.h>
 #include <LibWeb/CSS/StyleInvalidation.h>
 #include <LibWeb/CSS/StyleProperty.h>
@@ -399,6 +400,12 @@ public:
     void set_in_top_layer(bool in_top_layer) { m_in_top_layer = in_top_layer; }
     bool in_top_layer() const { return m_in_top_layer; }
 
+    bool has_non_empty_counters_set() const { return m_counters_set; }
+    Optional<CSS::CountersSet const&> counters_set();
+    CSS::CountersSet& ensure_counters_set();
+    void resolve_counters(CSS::StyleProperties&);
+    void inherit_counters();
+
 protected:
     Element(Document&, DOM::QualifiedName);
     virtual void initialize(JS::Realm&) override;
@@ -476,6 +483,8 @@ private:
     Array<CSSPixelPoint, 3> m_scroll_offset;
 
     bool m_in_top_layer { false };
+
+    OwnPtr<CSS::CountersSet> m_counters_set;
 };
 
 template<>

+ 2 - 0
Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp

@@ -334,9 +334,11 @@ void TreeBuilder::create_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
         element.clear_pseudo_element_nodes({});
         VERIFY(!element.needs_style_update());
         style = element.computed_css_values();
+        element.resolve_counters(*style);
         display = style->display();
         if (display.is_none())
             return;
+        // TODO: Implement changing element contents with the `content` property.
         if (context.layout_svg_mask_or_clip_path) {
             if (is<SVG::SVGMaskElement>(dom_node))
                 layout_node = document.heap().allocate_without_realm<Layout::SVGMaskBox>(document, static_cast<SVG::SVGMaskElement&>(dom_node), *style);