Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 135 additions & 2 deletions Libraries/LibWeb/CSS/CountersSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/Math.h>
#include <AK/NeverDestroyed.h>
#include <AK/SaturatingMath.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/CountersSet.h>
Expand Down Expand Up @@ -85,6 +87,16 @@ void CountersSet::increment_a_counter(Utf16FlyString name, DOM::AbstractElement
counter.value = saturating_add(*counter.value, amount);
}

// https://drafts.csswg.org/css-lists-3/#valdef-counter-set-counter-name-integer
// "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."
CounterValue CountersSet::counter_value_for_use(Utf16FlyString const& name, DOM::AbstractElement const& element)
{
if (auto counter = last_counter_with_name(name); counter.has_value())
return counter->value.value_or(0);
return *instantiate_a_counter(name, element, false, 0).value;
}

Optional<Counter&> CountersSet::last_counter_with_name(Utf16FlyString const& name)
{
for (auto& counter : m_counters.in_reverse()) {
Expand All @@ -106,6 +118,116 @@ void CountersSet::append_copy(Counter const& counter)
m_counters.append(counter);
}

Utf16FlyString const& list_item_counter_name()
{
static NeverDestroyed<Utf16FlyString> name = "list-item"_utf16_fly_string;
return *name;
}

// https://drafts.csswg.org/css-lists-3/#list-item-counter
// "Specifically, unless the counter-increment property explicitly specifies a different increment
// for the list-item counter, it must be incremented by 1 on every list item, or if the counter is
// reversed, it must be incremented by -1 on every list item instead, at the same time that counters
// are normally incremented (exactly as if the list item had list-item 1 or list-item -1 appended to
// their counter-increment value, including side-effects such as possibly instantiating a new
// counter, etc)."
static bool style_has_implicit_list_item_increment(ComputedValues const& style)
{
if (!style.display().is_list_item())
return false;
for (auto const& counter : style.counter_increment()) {
if (counter.name == list_item_counter_name())
return false;
}
return true;
}

enum class ReversedScopeWalkDecision : u8 {
Continue,
Stop,
};

struct ReversedScopeWalkState {
Utf16FlyString const& name;
bool name_is_list_item { false };
i64 num { 0 };
i64 last_nonzero_increment_negated { 0 };
};

static ReversedScopeWalkDecision apply_reversed_counter_contribution(ReversedScopeWalkState& state, ComputedValues const& style)
{
i64 increment = 0;
for (auto const& counter : style.counter_increment()) {
if (counter.name == state.name)
increment += *counter.value;
}
if (state.name_is_list_item && style_has_implicit_list_item_increment(style))
increment = -1;

auto increment_negated = -increment;
if (increment_negated != 0)
state.last_nonzero_increment_negated = increment_negated;

for (auto const& counter : style.counter_set()) {
if (counter.name == state.name) {
state.num += *counter.value;
return ReversedScopeWalkDecision::Stop;
}
}
state.num += increment_negated;
return ReversedScopeWalkDecision::Continue;
}

static ReversedScopeWalkDecision walk_reversed_counter_sibling_run(ReversedScopeWalkState& state, DOM::Element* first)
{
for (auto* element = first; element; element = element->next_element_sibling()) {
auto style = element->computed_values();
if (!style || style->display().is_none())
continue;
bool resets_name = false;
for (auto const& counter : style->counter_reset()) {
if (counter.name == state.name) {
resets_name = true;
break;
}
}
if (resets_name)
break;
if (apply_reversed_counter_contribution(state, *style) == ReversedScopeWalkDecision::Stop)
return ReversedScopeWalkDecision::Stop;
if (walk_reversed_counter_sibling_run(state, element->first_element_child()) == ReversedScopeWalkDecision::Stop)
return ReversedScopeWalkDecision::Stop;
}
return ReversedScopeWalkDecision::Continue;
}

// https://drafts.csswg.org/css-lists-3/#instantiating-counters
// "When a counter is instantiated without an initial value, the user agent must dynamically
// calculate the initial value at layout-time to be the value returned by the following algorithm:
// 1. Let num be 0.
// 2. Let lastNonZeroIncrementNegated be 0.
// 3. For each element or pseudo-element el that increments or sets the same counter in the same scope:
// 1. Let incrementNegated be el's counter-increment integer value for this counter, multiplied by -1.
// 2. If incrementNegated is not zero, then set lastNonZeroIncrementNegated to incrementNegated.
// 3. If el sets this counter with counter-set, then add that integer value to num and break this loop.
// 4. Add incrementNegated to num.
// 4. Add lastNonZeroIncrementNegated to num.
// 5. Return num."
static CounterValue reversed_counter_start_value(Utf16FlyString const& name, DOM::AbstractElement& originating_element, ComputedValues const& originating_style)
{
ReversedScopeWalkState state { .name = name, .name_is_list_item = name == list_item_counter_name() };
auto decision = apply_reversed_counter_contribution(state, originating_style);
// FIXME: Counters reset on pseudo-elements don't walk a scope yet; only the pseudo-element's own
// contribution is taken into account.
if (decision == ReversedScopeWalkDecision::Continue && !originating_element.pseudo_element().has_value()) {
auto& element = originating_element.element();
decision = walk_reversed_counter_sibling_run(state, element.first_element_child());
if (decision == ReversedScopeWalkDecision::Continue)
walk_reversed_counter_sibling_run(state, element.next_element_sibling());
}
return AK::clamp_to<CounterValue>(state.num + state.last_nonzero_increment_negated);
}

// https://drafts.csswg.org/css-lists-3/#auto-numbering
void resolve_counters(DOM::AbstractElement& element_reference)
{
Expand All @@ -123,8 +245,12 @@ void resolve_counters(DOM::AbstractElement& element_reference)
return;

// 2. New counters are instantiated (counter-reset).
for (auto const& counter : style.counter_reset())
element_reference.ensure_counters_set().instantiate_a_counter(counter.name, element_reference, counter.is_reversed, counter.value);
for (auto const& counter : style.counter_reset()) {
auto value = counter.value;
if (counter.is_reversed && !value.has_value())
value = reversed_counter_start_value(counter.name, element_reference, style);
element_reference.ensure_counters_set().instantiate_a_counter(counter.name, element_reference, counter.is_reversed, value);
}

// FIXME: Take style containment into account
// https://drafts.csswg.org/css-contain-2/#containment-style
Expand All @@ -136,6 +262,13 @@ void resolve_counters(DOM::AbstractElement& element_reference)
for (auto const& counter : style.counter_increment())
element_reference.ensure_counters_set().increment_a_counter(counter.name, element_reference, *counter.value);

if (style_has_implicit_list_item_increment(style)) {
auto& counters = element_reference.ensure_counters_set();
auto innermost_list_item_counter = counters.last_counter_with_name(list_item_counter_name());
bool reversed = innermost_list_item_counter.has_value() && innermost_list_item_counter->reversed;
counters.increment_a_counter(list_item_counter_name(), element_reference, reversed ? -1 : 1);
}

// 4. Counter values are explicitly set (counter-set).
for (auto const& counter : style.counter_set())
element_reference.ensure_counters_set().set_a_counter(counter.name, element_reference, *counter.value);
Expand Down
3 changes: 3 additions & 0 deletions Libraries/LibWeb/CSS/CountersSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class CountersSet {
Counter& instantiate_a_counter(Utf16FlyString name, DOM::AbstractElement const&, bool reversed, Optional<CounterValue>);
void set_a_counter(Utf16FlyString name, DOM::AbstractElement const&, CounterValue value);
void increment_a_counter(Utf16FlyString name, DOM::AbstractElement const&, CounterValue amount);
CounterValue counter_value_for_use(Utf16FlyString const& name, DOM::AbstractElement const&);
void append_copy(Counter const&);

Optional<Counter&> last_counter_with_name(Utf16FlyString const& name);
Expand All @@ -54,4 +55,6 @@ class CountersSet {
void resolve_counters(DOM::AbstractElement&);
void inherit_counters(DOM::AbstractElement&);

Utf16FlyString const& list_item_counter_name();

}
10 changes: 10 additions & 0 deletions Libraries/LibWeb/CSS/Default.css
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,16 @@ details > summary:first-of-type {
list-style: disclosure-closed inside;
}

/* https://drafts.csswg.org/css-lists-3/#marker-properties
* "UAs must add the following rule to their default style sheet:"
* FIXME: Also match ::before::marker and ::after::marker once compound pseudo-element selectors are supported. */
::marker {
unicode-bidi: isolate;
font-variant-numeric: tabular-nums;
white-space: pre;
text-transform: none;
}

details[open] > summary:first-of-type {
list-style-type: disclosure-open;
}
Expand Down
9 changes: 3 additions & 6 deletions Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -862,9 +862,6 @@ RefPtr<StyleValue const> Parser::parse_counter_definitions_value(TokenStream<Com
// Otherwise parses:
// [ <counter-name> <integer>? ]+

// FIXME: This disabled parsing of `reversed()` counters. Remove this line once they're supported.
allow_reversed = AllowReversed::No;

auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();

Expand All @@ -884,14 +881,14 @@ RefPtr<StyleValue const> Parser::parse_counter_definitions_value(TokenStream<Com
TokenStream function_tokens { token.function().value };
tokens.discard_a_token();
function_tokens.discard_whitespace();
auto& name_token = function_tokens.consume_a_token();
if (!name_token.is(Token::Type::Ident))
auto counter_name = parse_custom_ident_value(function_tokens, { { "none"sv } });
if (!counter_name)
break;
function_tokens.discard_whitespace();
if (function_tokens.has_next_token())
break;

definition.name = name_token.token().ident();
definition.name = counter_name->custom_ident();
definition.is_reversed = true;
} else {
break;
Expand Down
7 changes: 5 additions & 2 deletions Libraries/LibWeb/DOM/AbstractElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,11 @@ Optional<AbstractElement> AbstractElement::walk_layout_tree(WalkMethod walk_meth
if (auto* previous_element = as_if<Element>(node->dom_node()))
return AbstractElement { *previous_element };

if (node->is_generated_for_pseudo_element())
return AbstractElement { *node->pseudo_element_generator(), node->generated_for_pseudo_element() };
if (node->is_generated_for_pseudo_element()) {
auto pseudo_element = node->generated_for_pseudo_element();
if (pseudo_element.has_value() && CSS::is_tree_abiding_pseudo_element(*pseudo_element))
return AbstractElement { *node->pseudo_element_generator(), pseudo_element };
}
}
}

Expand Down
Loading