mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-05 00:56:39 +00:00
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:
parent
a6df9e1bac
commit
c1d4323cf7
Notes:
github-actions[bot]
2025-06-19 11:36:57 +00:00
Author: https://github.com/AtkinsSJ
Commit: c1d4323cf7
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5132
Reviewed-by: https://github.com/tcl3
14 changed files with 247 additions and 60 deletions
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 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 = 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 counter’s 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 element’s CSS counters set named <counter-name>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue