From 708f49d906389f8875f006894e2aacf2305834bf Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 17 Jul 2024 11:46:08 +0100 Subject: [PATCH] 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. --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + Userland/Libraries/LibWeb/CSS/CountersSet.cpp | 103 ++++++++++++++++++ Userland/Libraries/LibWeb/CSS/CountersSet.h | 50 +++++++++ Userland/Libraries/LibWeb/DOM/Element.cpp | 88 +++++++++++++++ Userland/Libraries/LibWeb/DOM/Element.h | 9 ++ .../Libraries/LibWeb/Layout/TreeBuilder.cpp | 2 + 6 files changed, 253 insertions(+) create mode 100644 Userland/Libraries/LibWeb/CSS/CountersSet.cpp create mode 100644 Userland/Libraries/LibWeb/CSS/CountersSet.h diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index aabb576791a..2a3b82adf37 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/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 diff --git a/Userland/Libraries/LibWeb/CSS/CountersSet.cpp b/Userland/Libraries/LibWeb/CSS/CountersSet.cpp new file mode 100644 index 00000000000..6eacb705230 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/CountersSet.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "CountersSet.h" +#include +#include + +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 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(*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 CountersSet::last_counter_with_name(FlyString const& name) +{ + for (auto& counter : m_counters.in_reverse()) { + if (counter.name == name) + return counter; + } + return {}; +} + +Optional 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); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/CountersSet.h b/Userland/Libraries/LibWeb/CSS/CountersSet.h new file mode 100644 index 00000000000..092b7e248e6 --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/CountersSet.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +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 and saturating addition/subtraction. +using CounterValue = Checked; + +// https://drafts.csswg.org/css-lists-3/#counter +struct Counter { + FlyString name; + i32 originating_element_id; // "creator" + bool reversed { false }; + Optional 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); + 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 last_counter_with_name(FlyString const& name); + Optional counter_with_same_name_and_creator(FlyString const& name, i32 originating_element_id); + + Vector const& counters() const { return m_counters; } + bool is_empty() const { return m_counters.is_empty(); } + +private: + Vector m_counters; +}; + +} diff --git a/Userland/Libraries/LibWeb/DOM/Element.cpp b/Userland/Libraries/LibWeb/DOM/Element.cpp index b793212a0ee..7ac3ff5b1ae 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.cpp +++ b/Userland/Libraries/LibWeb/DOM/Element.cpp @@ -2696,4 +2696,92 @@ WebIDL::ExceptionOr Element::set_html_unsafe(StringView html) return {}; } +Optional 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(); + 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 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(); + }; + if (parent->has_non_empty_counters_set()) { + element_counters = make(); + *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(); 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); +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Element.h b/Userland/Libraries/LibWeb/DOM/Element.h index 014304f8f30..9a231a65df5 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.h +++ b/Userland/Libraries/LibWeb/DOM/Element.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -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 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 m_scroll_offset; bool m_in_top_layer { false }; + + OwnPtr m_counters_set; }; template<> diff --git a/Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp index 5b637f7d3e7..61e4f566fc4 100644 --- a/Userland/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/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(dom_node)) layout_node = document.heap().allocate_without_realm(document, static_cast(dom_node), *style);