From a57595faf5e8ffba82e5b24e0fbf344a9f6b2453 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 18 Jun 2025 10:28:56 +0100 Subject: [PATCH] LibWeb: Make [resolve,inherit]_counters() take AbstractElement This is one of those cases where the spec says "element" and means "element or pseudo-element". The easiest way to handle both is to make these be free functions that take an AbstractElement, and then give AbstractElement some helper methods so that the caller doesn't have to care which it's dealing with. There are some FIXMEs here because PseudoElement doesn't have a CountersSet yet, and because the CountersSet currently uses a UniqueNodeID to identify counter sources, which doesn't support pseudo-elements. --- Libraries/LibWeb/CSS/CountersSet.cpp | 108 +++++++++++++++++++++++- Libraries/LibWeb/CSS/CountersSet.h | 5 +- Libraries/LibWeb/DOM/Element.cpp | 99 +--------------------- Libraries/LibWeb/DOM/Element.h | 5 +- Libraries/LibWeb/Layout/TreeBuilder.cpp | 3 +- 5 files changed, 117 insertions(+), 103 deletions(-) diff --git a/Libraries/LibWeb/CSS/CountersSet.cpp b/Libraries/LibWeb/CSS/CountersSet.cpp index ff238f250bc..e7d147ec059 100644 --- a/Libraries/LibWeb/CSS/CountersSet.cpp +++ b/Libraries/LibWeb/CSS/CountersSet.cpp @@ -1,10 +1,12 @@ /* - * Copyright (c) 2024, Sam Atkins + * Copyright (c) 2024-2025, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ -#include "CountersSet.h" +#include +#include +#include #include #include @@ -100,4 +102,106 @@ void CountersSet::append_copy(Counter const& counter) m_counters.append(counter); } +// https://drafts.csswg.org/css-lists-3/#auto-numbering +void resolve_counters(DOM::AbstractElement& element_reference) +{ + // Resolving counter values on a given element is a multi-step process: + auto const& style = *element_reference.computed_properties(); + + // 1. Existing counters are inherited from previous elements. + inherit_counters(element_reference); + + // https://drafts.csswg.org/css-lists-3/#counters-without-boxes + // An element that does not generate a box (for example, an element with display set to none, + // or a pseudo-element with content set to none) cannot set, reset, or increment a counter. + // The counter properties are still valid on such an element, but they must have no effect. + if (style.display().is_none()) + return; + + // FIXME: Include the pseudo-element in the element ID + auto element_id = element_reference.element().unique_id(); + + // 2. New counters are instantiated (counter-reset). + auto counter_reset = style.counter_data(PropertyID::CounterReset); + for (auto const& counter : counter_reset) + element_reference.ensure_counters_set().instantiate_a_counter(counter.name, element_id, counter.is_reversed, counter.value); + + // FIXME: Take style containment into account + // https://drafts.csswg.org/css-contain-2/#containment-style + // Giving an element style containment has the following effects: + // 1. The 'counter-increment' and 'counter-set' properties must be scoped to the element’s sub-tree and create a + // new counter. + + // 3. Counter values are incremented (counter-increment). + auto counter_increment = style.counter_data(PropertyID::CounterIncrement); + for (auto const& counter : counter_increment) + element_reference.ensure_counters_set().increment_a_counter(counter.name, element_id, *counter.value); + + // 4. Counter values are explicitly set (counter-set). + auto counter_set = style.counter_data(PropertyID::CounterSet); + for (auto const& counter : counter_set) + element_reference.ensure_counters_set().set_a_counter(counter.name, element_id, *counter.value); + + // 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 inherit_counters(DOM::AbstractElement& element_reference) +{ + // 1. If element is the root of its document tree, the element has an initially-empty CSS counters set. + // Return. + auto parent = element_reference.parent_element(); + if (parent == nullptr) { + // NOTE: We represent an empty counters set with `m_counters_set = nullptr`. + element_reference.set_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->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 = element_reference.element().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 = element_reference.element().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()); + element_reference.set_counters_set(move(element_counters)); +} + } diff --git a/Libraries/LibWeb/CSS/CountersSet.h b/Libraries/LibWeb/CSS/CountersSet.h index 65496c86e1d..260a80720d8 100644 --- a/Libraries/LibWeb/CSS/CountersSet.h +++ b/Libraries/LibWeb/CSS/CountersSet.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Sam Atkins + * Copyright (c) 2024-2025, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -48,4 +48,7 @@ private: Vector m_counters; }; +void resolve_counters(DOM::AbstractElement&); +void inherit_counters(DOM::AbstractElement&); + } diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index ba8459875b7..05ed2226fa1 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -3802,7 +3802,7 @@ WebIDL::ExceptionOr Element::set_html_unsafe(StringView html) return {}; } -Optional Element::counters_set() +Optional Element::counters_set() const { if (!m_counters_set) return {}; @@ -3816,102 +3816,9 @@ CSS::CountersSet& Element::ensure_counters_set() return *m_counters_set; } -// https://drafts.csswg.org/css-lists-3/#auto-numbering -void Element::resolve_counters(CSS::ComputedProperties& style) +void Element::set_counters_set(OwnPtr&& counters_set) { - // Resolving counter values on a given element is a multi-step process: - - // 1. Existing counters are inherited from previous elements. - inherit_counters(); - - // https://drafts.csswg.org/css-lists-3/#counters-without-boxes - // An element that does not generate a box (for example, an element with display set to none, - // or a pseudo-element with content set to none) cannot set, reset, or increment a counter. - // The counter properties are still valid on such an element, but they must have no effect. - if (style.display().is_none()) - return; - - // 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); - - // FIXME: Take style containment into account - // https://drafts.csswg.org/css-contain-2/#containment-style - // Giving an element style containment has the following effects: - // 1. The 'counter-increment' and 'counter-set' properties must be scoped to the element’s sub-tree and create a - // new counter. - - // 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. -} - -// 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); + m_counters_set = move(counters_set); } // https://html.spec.whatwg.org/multipage/dom.html#the-lang-and-xml:lang-attributes diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 6647d657bf6..0945297d28c 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -418,10 +418,9 @@ public: bool rendered_in_top_layer() const { return m_rendered_in_top_layer; } bool has_non_empty_counters_set() const { return m_counters_set; } - Optional counters_set(); + Optional counters_set() const; CSS::CountersSet& ensure_counters_set(); - void resolve_counters(CSS::ComputedProperties&); - void inherit_counters(); + void set_counters_set(OwnPtr&&); ProximityToTheViewport proximity_to_the_viewport() const { return m_proximity_to_the_viewport; } void determine_proximity_to_the_viewport(); diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index 659f9bebeaa..02f60afa588 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -480,7 +480,8 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context& element.clear_pseudo_element_nodes({}); VERIFY(!element.needs_style_update()); style = element.computed_properties(); - element.resolve_counters(*style); + DOM::AbstractElement element_reference { element }; + CSS::resolve_counters(element_reference); display = style->display(); if (display.is_none()) return;