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;