LibWeb: Support counter-* properties on pseudo-elements

There are multiple things happening here which are interconnected:

- We now use AbstractElement to refer to the source of a counter, which
  means we also need to pass that around to compute `content`.

- Give AbstractElement new helper methods that are needed by
  CountersSet, so it doesn't have to care whether it's dealing with a
  true Element or PseudoElement.

- The CountersSet algorithms now walk the layout tree instead of DOM
  tree, so TreeBuilder needs to wait until the layout node exists
  before it resolves counters for it.

- Resolve counters when creating a pseudo-element's layout node. We
  awkwardly compute the `content` value up to twice: Once to figure out
  what kind of node we need to make, and then if it's a string, we do
  so again after counters are resolved so we can get the true value of
  any `counter()` functions. This will need adjusting in the future but
  it works for now.
This commit is contained in:
Sam Atkins 2025-06-17 16:33:23 +01:00
commit c1d4323cf7
Notes: github-actions[bot] 2025-06-19 11:36:57 +00:00
14 changed files with 247 additions and 60 deletions

View file

@ -935,7 +935,7 @@ ColumnSpan ComputedProperties::column_span() const
return keyword_to_column_span(value.to_keyword()).release_value();
}
ComputedProperties::ContentDataAndQuoteNestingLevel ComputedProperties::content(DOM::Element& element, u32 initial_quote_nesting_level) const
ComputedProperties::ContentDataAndQuoteNestingLevel ComputedProperties::content(DOM::AbstractElement& element_reference, u32 initial_quote_nesting_level) const
{
auto const& value = property(PropertyID::Content);
auto quotes_data = quotes();
@ -999,7 +999,7 @@ ComputedProperties::ContentDataAndQuoteNestingLevel ComputedProperties::content(
break;
}
} else if (item->is_counter()) {
builder.append(item->as_counter().resolve(element));
builder.append(item->as_counter().resolve(element_reference));
} else {
// TODO: Implement images, and other things.
dbgln("`{}` is not supported in `content` (yet?)", item->to_string(SerializationMode::Normal));
@ -1014,7 +1014,7 @@ ComputedProperties::ContentDataAndQuoteNestingLevel ComputedProperties::content(
if (item->is_string()) {
alt_text_builder.append(item->as_string().string_value());
} else if (item->is_counter()) {
alt_text_builder.append(item->as_counter().resolve(element));
alt_text_builder.append(item->as_counter().resolve(element_reference));
} else {
dbgln("`{}` is not supported in `content` alt-text (yet?)", item->to_string(SerializationMode::Normal));
}

View file

@ -96,7 +96,7 @@ public:
ContentData content_data;
u32 final_quote_nesting_level { 0 };
};
ContentDataAndQuoteNestingLevel content(DOM::Element&, u32 initial_quote_nesting_level) const;
ContentDataAndQuoteNestingLevel content(DOM::AbstractElement&, u32 initial_quote_nesting_level) const;
ContentVisibility content_visibility() const;
Vector<CursorData> cursor() const;
Variant<LengthOrCalculated, NumberOrCalculated> tab_size() const;

View file

@ -12,27 +12,30 @@
namespace Web::CSS {
void CountersSet::visit_edges(GC::Cell::Visitor& visitor)
{
for (auto const& counter : m_counters)
counter.originating_element.visit(visitor);
}
// https://drafts.csswg.org/css-lists-3/#instantiate-counter
Counter& CountersSet::instantiate_a_counter(FlyString name, UniqueNodeID originating_element_id, bool reversed, Optional<CounterValue> value)
Counter& CountersSet::instantiate_a_counter(FlyString name, DOM::AbstractElement const& element, bool reversed, Optional<CounterValue> value)
{
// 1. Let counters be elements 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 counters 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 = as<DOM::Element>(*originating_node);
auto& innermost_element = innermost_counter->originating_element;
if (&innermost_element == element
|| (innermost_element.parent() == element->parent() && innermost_element.is_before(*element))) {
if (innermost_element == element
|| (innermost_element.parent_element() == element.parent_element() && 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;
&& it.originating_element == innermost_counter->originating_element;
});
}
}
@ -41,7 +44,7 @@ Counter& CountersSet::instantiate_a_counter(FlyString name, UniqueNodeID origina
// reversed being reversed, and initial value value (if given)
m_counters.append({
.name = move(name),
.originating_element_id = originating_element_id,
.originating_element = element,
.reversed = reversed,
.value = value,
});
@ -50,7 +53,7 @@ Counter& CountersSet::instantiate_a_counter(FlyString name, UniqueNodeID origina
}
// https://drafts.csswg.org/css-lists-3/#propdef-counter-set
void CountersSet::set_a_counter(FlyString name, UniqueNodeID originating_element_id, CounterValue value)
void CountersSet::set_a_counter(FlyString name, DOM::AbstractElement const& element, CounterValue value)
{
if (auto existing_counter = last_counter_with_name(name); existing_counter.has_value()) {
existing_counter->value = value;
@ -60,12 +63,12 @@ void CountersSet::set_a_counter(FlyString name, UniqueNodeID originating_element
// 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);
auto& counter = instantiate_a_counter(name, element, false, 0);
counter.value = value;
}
// https://drafts.csswg.org/css-lists-3/#propdef-counter-increment
void CountersSet::increment_a_counter(FlyString name, UniqueNodeID originating_element_id, CounterValue amount)
void CountersSet::increment_a_counter(FlyString name, DOM::AbstractElement const& element, 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?
@ -77,7 +80,7 @@ void CountersSet::increment_a_counter(FlyString name, UniqueNodeID originating_e
// 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);
auto& counter = instantiate_a_counter(name, element, false, 0);
counter.value->saturating_add(amount.value());
}
@ -90,10 +93,10 @@ Optional<Counter&> CountersSet::last_counter_with_name(FlyString const& name)
return {};
}
Optional<Counter&> CountersSet::counter_with_same_name_and_creator(FlyString const& name, UniqueNodeID originating_element_id)
Optional<Counter&> CountersSet::counter_with_same_name_and_creator(FlyString const& name, DOM::AbstractElement const& element)
{
return m_counters.first_matching([&](auto& it) {
return it.name == name && it.originating_element_id == originating_element_id;
return it.name == name && it.originating_element == element;
});
}
@ -118,13 +121,10 @@ void resolve_counters(DOM::AbstractElement& element_reference)
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);
element_reference.ensure_counters_set().instantiate_a_counter(counter.name, element_reference, counter.is_reversed, counter.value);
// FIXME: Take style containment into account
// https://drafts.csswg.org/css-contain-2/#containment-style
@ -135,12 +135,12 @@ void resolve_counters(DOM::AbstractElement& element_reference)
// 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);
element_reference.ensure_counters_set().increment_a_counter(counter.name, element_reference, *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);
element_reference.ensure_counters_set().set_a_counter(counter.name, element_reference, *counter.value);
// 5. Counter values are used (counter()/counters()).
// NOTE: This happens when we process the `content` property.
@ -176,7 +176,7 @@ void inherit_counters(DOM::AbstractElement& element_reference)
// 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<DOM::Element>(); sibling && sibling->has_non_empty_counters_set()) {
if (auto sibling = element_reference.previous_sibling_in_tree_order(); sibling.has_value() && sibling->has_non_empty_counters_set()) {
auto& sibling_counters = sibling->counters_set().release_value();
ensure_element_counters();
for (auto const& counter : sibling_counters.counters()) {
@ -188,12 +188,12 @@ void inherit_counters(DOM::AbstractElement& element_reference)
// 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 counters value.
if (auto* const previous = element_reference.element().previous_element_in_pre_order(); previous && previous->has_non_empty_counters_set()) {
if (auto const previous = element_reference.previous_in_tree_order(); previous.has_value() && 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);
auto maybe_existing_counter = element_counters->counter_with_same_name_and_creator(source_counter.name, source_counter.originating_element);
if (maybe_existing_counter.has_value())
maybe_existing_counter->value = source_counter.value;
}
@ -204,4 +204,15 @@ void inherit_counters(DOM::AbstractElement& element_reference)
element_reference.set_counters_set(move(element_counters));
}
String CountersSet::dump() const
{
StringBuilder builder;
builder.append("{\n"sv);
for (auto const& counter : m_counters) {
builder.appendff(" {} ({}) = {}\n", counter.name, counter.originating_element.debug_description(), counter.value);
}
builder.append('}');
return builder.to_string_without_validation();
}
}

View file

@ -9,6 +9,7 @@
#include <AK/Checked.h>
#include <AK/FlyString.h>
#include <AK/Optional.h>
#include <LibWeb/DOM/AbstractElement.h>
#include <LibWeb/Forward.h>
namespace Web::CSS {
@ -22,7 +23,7 @@ using CounterValue = Checked<i32>;
// https://drafts.csswg.org/css-lists-3/#counter
struct Counter {
FlyString name;
UniqueNodeID originating_element_id; // "creator"
DOM::AbstractElement originating_element; // "creator"
bool reversed { false };
Optional<CounterValue> value;
};
@ -33,17 +34,21 @@ public:
CountersSet() = default;
~CountersSet() = default;
Counter& instantiate_a_counter(FlyString name, UniqueNodeID originating_element_id, bool reversed, Optional<CounterValue>);
void set_a_counter(FlyString name, UniqueNodeID originating_element_id, CounterValue value);
void increment_a_counter(FlyString name, UniqueNodeID originating_element_id, CounterValue amount);
Counter& instantiate_a_counter(FlyString name, DOM::AbstractElement const&, bool reversed, Optional<CounterValue>);
void set_a_counter(FlyString name, DOM::AbstractElement const&, CounterValue value);
void increment_a_counter(FlyString name, DOM::AbstractElement const&, 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, UniqueNodeID originating_element_id);
Optional<Counter&> counter_with_same_name_and_creator(FlyString const& name, DOM::AbstractElement const&);
Vector<Counter> const& counters() const { return m_counters; }
bool is_empty() const { return m_counters.is_empty(); }
void visit_edges(GC::Cell::Visitor&);
String dump() const;
private:
Vector<Counter> m_counters;
};

View file

@ -1,15 +1,16 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024-2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "CounterStyleValue.h"
#include <LibWeb/CSS/CountersSet.h>
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/CSS/Keyword.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/CounterStyleValue.h>
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/DOM/Element.h>
@ -94,13 +95,13 @@ static String generate_a_counter_representation(CSSStyleValue const& counter_sty
return MUST(String::formatted("{}", value));
}
String CounterStyleValue::resolve(DOM::Element& element) const
String CounterStyleValue::resolve(DOM::AbstractElement& element_reference) const
{
// "If no counter named <counter-name> exists on an element where counter() or counters() is used,
// one is first instantiated with a starting value of 0."
auto& counters_set = element.ensure_counters_set();
auto& counters_set = element_reference.ensure_counters_set();
if (!counters_set.last_counter_with_name(m_properties.counter_name).has_value())
counters_set.instantiate_a_counter(m_properties.counter_name, element.unique_id(), false, 0);
counters_set.instantiate_a_counter(m_properties.counter_name, element_reference, false, 0);
// counter( <counter-name>, <counter-style>? )
// "Represents the value of the innermost counter in the elements CSS counters set named <counter-name>

View file

@ -34,7 +34,7 @@ public:
auto counter_style() const { return m_properties.counter_style; }
auto join_string() const { return m_properties.join_string; }
String resolve(DOM::Element&) const;
String resolve(DOM::AbstractElement&) const;
virtual String to_string(SerializationMode) const override;

View file

@ -6,6 +6,7 @@
#include <LibWeb/DOM/AbstractElement.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/Layout/Node.h>
namespace Web::DOM {
@ -20,6 +21,13 @@ void AbstractElement::visit(GC::Cell::Visitor& visitor) const
visitor.visit(m_element);
}
GC::Ptr<Layout::NodeWithStyle> AbstractElement::layout_node()
{
if (m_pseudo_element.has_value())
return m_element->get_pseudo_element_node(*m_pseudo_element);
return m_element->layout_node();
}
GC::Ptr<Element const> AbstractElement::parent_element() const
{
if (m_pseudo_element.has_value())
@ -27,6 +35,39 @@ GC::Ptr<Element const> AbstractElement::parent_element() const
return m_element->parent_element();
}
Optional<AbstractElement> AbstractElement::walk_layout_tree(WalkMethod walk_method)
{
GC::Ptr<Layout::Node> node = layout_node();
if (!node)
return OptionalNone {};
while (true) {
switch (walk_method) {
case WalkMethod::Previous:
node = node->previous_in_pre_order();
break;
case WalkMethod::PreviousSibling:
node = node->previous_sibling();
break;
}
if (!node)
return OptionalNone {};
if (auto* previous_element = as_if<Element>(node->dom_node()))
return AbstractElement { *previous_element };
if (node->is_generated())
return AbstractElement { *node->pseudo_element_generator(), node->generated_for_pseudo_element() };
}
}
bool AbstractElement::is_before(AbstractElement const& other) const
{
auto this_node = layout_node();
auto other_node = other.layout_node();
return this_node && other_node && this_node->is_before(*other_node);
}
GC::Ptr<CSS::ComputedProperties const> AbstractElement::computed_properties() const
{
if (m_pseudo_element.has_value())
@ -34,16 +75,46 @@ GC::Ptr<CSS::ComputedProperties const> AbstractElement::computed_properties() co
return m_element->computed_properties();
}
bool AbstractElement::has_non_empty_counters_set() const
{
if (m_pseudo_element.has_value())
return m_element->get_pseudo_element(*m_pseudo_element)->has_non_empty_counters_set();
return m_element->has_non_empty_counters_set();
}
Optional<CSS::CountersSet const&> AbstractElement::counters_set() const
{
if (m_pseudo_element.has_value())
return m_element->get_pseudo_element(*m_pseudo_element)->counters_set();
return m_element->counters_set();
}
CSS::CountersSet& AbstractElement::ensure_counters_set()
{
// FIXME: Handle pseudo-elements
if (m_pseudo_element.has_value())
return m_element->get_pseudo_element(*m_pseudo_element)->ensure_counters_set();
return m_element->ensure_counters_set();
}
void AbstractElement::set_counters_set(OwnPtr<CSS::CountersSet>&& counters_set)
{
// FIXME: Handle pseudo-elements
m_element->set_counters_set(move(counters_set));
if (m_pseudo_element.has_value()) {
m_element->get_pseudo_element(*m_pseudo_element)->set_counters_set(move(counters_set));
} else {
m_element->set_counters_set(move(counters_set));
}
}
String AbstractElement::debug_description() const
{
if (m_pseudo_element.has_value()) {
StringBuilder builder;
builder.append(m_element->debug_description());
builder.append("::"sv);
builder.append(CSS::pseudo_element_name(*m_pseudo_element));
return builder.to_string_without_validation();
}
return m_element->debug_description();
}
}

View file

@ -21,15 +21,33 @@ public:
Element const& element() const { return m_element; }
Optional<CSS::PseudoElement> pseudo_element() const { return m_pseudo_element; }
GC::Ptr<Layout::NodeWithStyle> layout_node();
GC::Ptr<Layout::NodeWithStyle const> layout_node() const { return const_cast<AbstractElement*>(this)->layout_node(); }
GC::Ptr<Element const> parent_element() const;
Optional<AbstractElement> previous_in_tree_order() { return walk_layout_tree(WalkMethod::Previous); }
Optional<AbstractElement> previous_sibling_in_tree_order() { return walk_layout_tree(WalkMethod::PreviousSibling); }
bool is_before(AbstractElement const&) const;
GC::Ptr<CSS::ComputedProperties const> computed_properties() const;
bool has_non_empty_counters_set() const;
Optional<CSS::CountersSet const&> counters_set() const;
CSS::CountersSet& ensure_counters_set();
void set_counters_set(OwnPtr<CSS::CountersSet>&&);
void visit(GC::Cell::Visitor& visitor) const;
String debug_description() const;
bool operator==(AbstractElement const&) const = default;
private:
enum class WalkMethod : u8 {
Previous,
PreviousSibling,
};
Optional<AbstractElement> walk_layout_tree(WalkMethod);
GC::Ref<Element> m_element;
Optional<CSS::PseudoElement> m_pseudo_element;
};

View file

@ -21,6 +21,7 @@
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/CSSStyleProperties.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/CountersSet.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/SelectorEngine.h>
@ -128,6 +129,8 @@ void Element::visit_edges(Cell::Visitor& visitor)
for (auto& registered_intersection_observers : *m_registered_intersection_observers)
visitor.visit(registered_intersection_observers.observer);
}
if (m_counters_set)
m_counters_set->visit_edges(visitor);
}
// https://dom.spec.whatwg.org/#dom-element-getattribute

View file

@ -16,7 +16,6 @@
#include <LibWeb/Bindings/ShadowRootPrototype.h>
#include <LibWeb/CSS/CascadedProperties.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/CountersSet.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleInvalidation.h>
#include <LibWeb/CSS/StyleProperty.h>

View file

@ -21,6 +21,8 @@ void PseudoElement::visit_edges(JS::Cell::Visitor& visitor)
visitor.visit(m_cascaded_properties);
visitor.visit(m_computed_properties);
visitor.visit(m_layout_node);
if (m_counters_set)
m_counters_set->visit_edges(visitor);
}
Optional<CSS::CountersSet const&> PseudoElement::counters_set() const

View file

@ -189,7 +189,8 @@ void TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element, CSS::Ps
return;
auto initial_quote_nesting_level = m_quote_nesting_level;
auto [pseudo_element_content, final_quote_nesting_level] = pseudo_element_style->content(element, initial_quote_nesting_level);
DOM::AbstractElement element_reference { element, pseudo_element };
auto [pseudo_element_content, final_quote_nesting_level] = pseudo_element_style->content(element_reference, initial_quote_nesting_level);
m_quote_nesting_level = final_quote_nesting_level;
auto pseudo_element_display = pseudo_element_style->display();
// ::before and ::after only exist if they have content. `content: normal` computes to `none` for them.
@ -217,27 +218,37 @@ void TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element, CSS::Ps
static_cast<ListItemBox&>(*pseudo_element_node).set_marker(list_item_marker);
element.set_pseudo_element_node({}, CSS::PseudoElement::Marker, list_item_marker);
pseudo_element_node->prepend_child(*list_item_marker);
// FIXME: Support counters on element::pseudo::marker
}
pseudo_element_node->set_generated_for(pseudo_element, element);
pseudo_element_node->set_initial_quote_nesting_level(initial_quote_nesting_level);
// FIXME: Handle images, and multiple values
if (pseudo_element_content.type == CSS::ContentData::Type::String) {
auto text = document.realm().create<DOM::Text>(document, pseudo_element_content.data);
auto text_node = document.heap().allocate<Layout::TextNode>(document, *text);
text_node->set_generated_for(pseudo_element, element);
push_parent(*pseudo_element_node);
insert_node_into_inline_or_block_ancestor(*text_node, text_node->display(), AppendOrPrepend::Append);
pop_parent();
} else {
TODO();
}
element.set_pseudo_element_node({}, pseudo_element, pseudo_element_node);
insert_node_into_inline_or_block_ancestor(*pseudo_element_node, pseudo_element_display, mode);
pseudo_element_node->mutable_computed_values().set_content(pseudo_element_content);
DOM::AbstractElement pseudo_element_reference { element, pseudo_element };
CSS::resolve_counters(pseudo_element_reference);
// Now that we have counters, we can compute the content for real. Which is silly.
if (pseudo_element_content.type == CSS::ContentData::Type::String) {
auto [new_content, _] = pseudo_element_style->content(element_reference, initial_quote_nesting_level);
pseudo_element_node->mutable_computed_values().set_content(new_content);
// FIXME: Handle images, and multiple values
if (new_content.type == CSS::ContentData::Type::String) {
auto text = document.realm().create<DOM::Text>(document, new_content.data);
auto text_node = document.heap().allocate<TextNode>(document, *text);
text_node->set_generated_for(pseudo_element, element);
push_parent(*pseudo_element_node);
insert_node_into_inline_or_block_ancestor(*text_node, text_node->display(), AppendOrPrepend::Append);
pop_parent();
} else {
TODO();
}
}
}
// Block nodes inside inline nodes are allowed, but to maintain the invariant that either all layout children are
@ -479,8 +490,6 @@ 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();
DOM::AbstractElement element_reference { element };
CSS::resolve_counters(element_reference);
display = style->display();
if (display.is_none())
return;
@ -539,8 +548,15 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
auto prior_quote_nesting_level = m_quote_nesting_level;
if (should_create_layout_node)
if (should_create_layout_node) {
// Resolve counters now that we exist in the layout tree.
if (auto* element = as_if<DOM::Element>(dom_node)) {
DOM::AbstractElement element_reference { *element };
CSS::resolve_counters(element_reference);
}
update_layout_tree_before_children(dom_node, *layout_node, context, element_has_content_visibility_hidden);
}
if (should_create_layout_node || dom_node.child_needs_layout_tree_update()) {
if ((dom_node.has_children() || shadow_root) && layout_node->can_have_children() && !element_has_content_visibility_hidden) {
@ -584,6 +600,9 @@ void TreeBuilder::update_layout_tree(DOM::Node& dom_node, TreeBuilder::Context&
top_layer_element->set_pseudo_element_node({}, CSS::PseudoElement::Backdrop, pseudo_element_node);
pseudo_element_node->set_generated_for(CSS::PseudoElement::Backdrop, top_layer_element);
insert_node_into_inline_or_block_ancestor(*pseudo_element_node, pseudo_element_display, AppendOrPrepend::Append);
DOM::AbstractElement backdrop_reference { top_layer_element, CSS::PseudoElement::Backdrop };
CSS::resolve_counters(backdrop_reference);
}();
update_layout_tree(top_layer_element, context, should_create_layout_node ? MustCreateSubtree::Yes : MustCreateSubtree::No);
}
@ -726,6 +745,8 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref
element.set_pseudo_element_computed_properties(CSS::PseudoElement::Marker, marker_style);
element.set_pseudo_element_node({}, CSS::PseudoElement::Marker, list_item_marker);
layout_node->prepend_child(*list_item_marker);
DOM::AbstractElement marker_reference { element, CSS::PseudoElement::Marker };
CSS::resolve_counters(marker_reference);
}
if (is<SVG::SVGGraphicsElement>(dom_node)) {

View file

@ -0,0 +1,46 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x70 [BFC] children: not-inline
BlockContainer <body> at (8,8) content-size 784x54 children: not-inline
BlockContainer <div#a> at (8,8) content-size 784x18 children: inline
frag 0 from TextNode start: 0, length: 1, rect: [26.6875,8 9.34375x18] baseline: 13.796875
"a"
InlineNode <(anonymous)>
frag 0 from TextNode start: 0, length: 3, rect: [8,8 18.6875x18] baseline: 13.796875
"1. "
TextNode <#text>
TextNode <#text>
BlockContainer <div#b> at (8,26) content-size 784x18 children: inline
frag 0 from TextNode start: 0, length: 1, rect: [29.15625,26 9.46875x18] baseline: 13.796875
"b"
InlineNode <(anonymous)>
frag 0 from TextNode start: 0, length: 3, rect: [8,26 21.15625x18] baseline: 13.796875
"2. "
TextNode <#text>
TextNode <#text>
BlockContainer <div#c> at (8,44) content-size 784x18 children: inline
frag 0 from TextNode start: 0, length: 1, rect: [29.4375,44 8.890625x18] baseline: 13.796875
"c"
InlineNode <(anonymous)>
frag 0 from TextNode start: 0, length: 3, rect: [8,44 21.4375x18] baseline: 13.796875
"3. "
TextNode <#text>
TextNode <#text>
BlockContainer <(anonymous)> at (8,62) content-size 784x0 children: inline
TextNode <#text>
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x70]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x54]
PaintableWithLines (BlockContainer<DIV>#a) [8,8 784x18]
PaintableWithLines (InlineNode(anonymous))
TextPaintable (TextNode<#text>)
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<DIV>#b) [8,26 784x18]
PaintableWithLines (InlineNode(anonymous))
TextPaintable (TextNode<#text>)
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<DIV>#c) [8,44 784x18]
PaintableWithLines (InlineNode(anonymous))
TextPaintable (TextNode<#text>)
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer(anonymous)) [8,62 784x0]

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<style>
body {
counter-reset: section;
}
div::before {
counter-increment: section;
content: counter(section) ". ";
}
</style><div id=a>a</div><div id=b>b</div><div id=c>c</div>