2026-02-05 22:37:32 +13:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2026, Callum Law <callumlaw1709@outlook.com>
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "CounterStyleDefinition.h"
|
|
|
|
|
#include <LibWeb/CSS/CSSCounterStyleRule.h>
|
|
|
|
|
#include <LibWeb/CSS/Enums.h>
|
|
|
|
|
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
|
|
|
|
|
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
|
|
|
|
|
|
|
|
|
|
namespace Web::CSS {
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#valdef-counter-style-range-auto
|
|
|
|
|
Vector<CounterStyleRangeEntry> AutoRange::resolve(CounterStyleAlgorithm const& algorithm)
|
|
|
|
|
{
|
|
|
|
|
// The range depends on the counter system:
|
|
|
|
|
return algorithm.visit(
|
2026-02-24 22:26:07 +13:00
|
|
|
[](AdditiveCounterStyleAlgorithm const&) -> Vector<CounterStyleRangeEntry> {
|
2026-02-05 22:37:32 +13:00
|
|
|
// For additive systems, the range is 0 to positive infinity.
|
2026-03-26 21:33:07 +13:00
|
|
|
return { { 0, NumericLimits<i32>::max() } };
|
2026-02-05 22:37:32 +13:00
|
|
|
},
|
2026-02-24 22:26:07 +13:00
|
|
|
[](FixedCounterStyleAlgorithm const&) -> Vector<CounterStyleRangeEntry> {
|
2026-02-05 22:37:32 +13:00
|
|
|
// For cyclic, numeric, and fixed systems, the range is negative infinity to positive infinity.
|
|
|
|
|
// NB: cyclic and numeric are handled below.
|
2026-03-26 21:33:07 +13:00
|
|
|
return { { NumericLimits<i32>::min(), NumericLimits<i32>::max() } };
|
2026-02-05 22:37:32 +13:00
|
|
|
},
|
2026-02-24 22:26:07 +13:00
|
|
|
[](GenericCounterStyleAlgorithm const& generic_algorithm) -> Vector<CounterStyleRangeEntry> {
|
2026-02-05 22:37:32 +13:00
|
|
|
switch (generic_algorithm.type) {
|
|
|
|
|
case CounterStyleSystem::Cyclic:
|
|
|
|
|
case CounterStyleSystem::Numeric:
|
|
|
|
|
// For cyclic, numeric, and fixed systems, the range is negative infinity to positive infinity.
|
|
|
|
|
// NB: Fixed is handled above.
|
2026-03-26 21:33:07 +13:00
|
|
|
return { { NumericLimits<i32>::min(), NumericLimits<i32>::max() } };
|
2026-02-05 22:37:32 +13:00
|
|
|
case CounterStyleSystem::Alphabetic:
|
|
|
|
|
case CounterStyleSystem::Symbolic:
|
|
|
|
|
// For alphabetic and symbolic systems, the range is 1 to positive infinity.
|
2026-03-26 21:33:07 +13:00
|
|
|
return { { 1, NumericLimits<i32>::max() } };
|
2026-02-05 22:37:32 +13:00
|
|
|
case CounterStyleSystem::Additive:
|
|
|
|
|
// NB: Additive is handled above.
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
}
|
|
|
|
|
VERIFY_NOT_REACHED();
|
2026-02-24 22:26:07 +13:00
|
|
|
},
|
|
|
|
|
[](EthiopicNumericCounterStyleAlgorithm const&) -> Vector<CounterStyleRangeEntry> {
|
|
|
|
|
// NB: All complex predefined counter styles define their range explicitly (i.e. not via auto)
|
|
|
|
|
VERIFY_NOT_REACHED();
|
2026-02-26 11:57:54 +13:00
|
|
|
},
|
|
|
|
|
[](ExtendedCJKCounterStyleAlgorithm const&) -> Vector<CounterStyleRangeEntry> {
|
|
|
|
|
// NB: All complex predefined counter styles define their range explicitly (i.e. not via auto)
|
|
|
|
|
VERIFY_NOT_REACHED();
|
2026-02-05 22:37:32 +13:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Optional<CounterStyleDefinition> CounterStyleDefinition::from_counter_style_rule(CSSCounterStyleRule const& rule, ComputationContext const& computation_context)
|
|
|
|
|
{
|
2026-02-27 13:55:33 +13:00
|
|
|
auto system_style_value = rule.system_style_value() ? NonnullRefPtr<StyleValue const> { *rule.system_style_value() } : CounterStyleSystemStyleValue::create(CounterStyleSystem::Symbolic);
|
2026-02-05 22:37:32 +13:00
|
|
|
|
2026-02-27 13:55:33 +13:00
|
|
|
auto maybe_algorithm = resolve_algorithm(system_style_value, rule.symbols_style_value(), rule.additive_symbols_style_value(), computation_context);
|
2026-02-05 22:37:32 +13:00
|
|
|
|
|
|
|
|
if (maybe_algorithm.has<Empty>())
|
|
|
|
|
return {};
|
|
|
|
|
|
2026-02-24 10:05:49 +13:00
|
|
|
return CounterStyleDefinition::create(
|
2026-02-05 22:37:32 +13:00
|
|
|
rule.name(),
|
|
|
|
|
maybe_algorithm.downcast<CounterStyleAlgorithm, CounterStyleSystemStyleValue::Extends>(),
|
|
|
|
|
rule.negative_style_value() ? Optional<CounterStyleNegativeSign> { resolve_negative_sign(*rule.negative_style_value()) } : Optional<CounterStyleNegativeSign> {},
|
|
|
|
|
rule.prefix_style_value() ? Optional<CounterStyleSymbol> { string_from_style_value(*rule.prefix_style_value()) } : Optional<CounterStyleSymbol> {},
|
|
|
|
|
rule.suffix_style_value() ? Optional<CounterStyleSymbol> { string_from_style_value(*rule.suffix_style_value()) } : Optional<CounterStyleSymbol> {},
|
|
|
|
|
rule.range_style_value() ? Variant<Empty, AutoRange, Vector<CounterStyleRangeEntry>> { resolve_range(*rule.range_style_value(), computation_context) } : Variant<Empty, AutoRange, Vector<CounterStyleRangeEntry>> {},
|
|
|
|
|
rule.fallback_style_value() ? Optional<FlyString> { string_from_style_value(*rule.fallback_style_value()) } : Optional<FlyString> {},
|
|
|
|
|
rule.pad_style_value() ? Optional<CounterStylePad> { resolve_pad(*rule.pad_style_value(), computation_context) } : Optional<CounterStylePad> {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#counter-style-system
|
|
|
|
|
Variant<Empty, CounterStyleAlgorithm, CounterStyleSystemStyleValue::Extends> CounterStyleDefinition::resolve_algorithm(NonnullRefPtr<StyleValue const> const& system_style_value, RefPtr<StyleValue const> const& symbols_style_value, RefPtr<StyleValue const> const& additive_symbols_style_value, ComputationContext const& computation_context)
|
|
|
|
|
{
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#counter-style-symbols
|
|
|
|
|
// The @counter-style rule must have a valid symbols descriptor if the counter system is cyclic,
|
|
|
|
|
// numeric, alphabetic, symbolic, or fixed, or a valid additive-symbols descriptor if the counter system
|
|
|
|
|
// is additive; otherwise, the @counter-style does not define a counter style (but is still a valid
|
|
|
|
|
// at-rule).
|
|
|
|
|
return system_style_value->as_counter_style_system().value().visit(
|
|
|
|
|
[&](CounterStyleSystem const& system) -> Variant<Empty, CounterStyleAlgorithm, CounterStyleSystemStyleValue::Extends> {
|
|
|
|
|
switch (system) {
|
|
|
|
|
case CounterStyleSystem::Cyclic:
|
|
|
|
|
case CounterStyleSystem::Alphabetic:
|
|
|
|
|
case CounterStyleSystem::Symbolic:
|
|
|
|
|
case CounterStyleSystem::Numeric: {
|
|
|
|
|
if (!symbols_style_value)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto parsed_symbols = resolve_symbols(*symbols_style_value);
|
|
|
|
|
|
|
|
|
|
if (!system_style_value->as_counter_style_system().is_valid_symbol_count(parsed_symbols.size()))
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
GenericCounterStyleAlgorithm {
|
|
|
|
|
.type = system,
|
|
|
|
|
.symbol_list = move(parsed_symbols),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
case CounterStyleSystem::Additive: {
|
|
|
|
|
if (!additive_symbols_style_value)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto additive_tuples = resolve_additive_symbols(*additive_symbols_style_value, computation_context);
|
|
|
|
|
|
|
|
|
|
if (!system_style_value->as_counter_style_system().is_valid_additive_symbol_count(additive_tuples.size()))
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
return { AdditiveCounterStyleAlgorithm { .symbol_list = move(additive_tuples) } };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
},
|
|
|
|
|
[&](CounterStyleSystemStyleValue::Fixed const& fixed) -> Variant<Empty, CounterStyleAlgorithm, CounterStyleSystemStyleValue::Extends> {
|
|
|
|
|
if (!symbols_style_value)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
auto parsed_symbols = resolve_symbols(*symbols_style_value);
|
|
|
|
|
|
|
|
|
|
if (!system_style_value->as_counter_style_system().is_valid_symbol_count(parsed_symbols.size()))
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#fixed-system
|
|
|
|
|
// If it is omitted, the first symbol value is 1.
|
2026-03-26 21:33:07 +13:00
|
|
|
i32 first_symbol = 1;
|
2026-02-05 22:37:32 +13:00
|
|
|
|
|
|
|
|
if (fixed.first_symbol)
|
|
|
|
|
first_symbol = int_from_style_value(fixed.first_symbol->absolutized(computation_context));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
FixedCounterStyleAlgorithm {
|
|
|
|
|
.first_symbol = first_symbol,
|
|
|
|
|
.symbol_list = move(parsed_symbols),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
[&](CounterStyleSystemStyleValue::Extends const& extends) -> Variant<Empty, CounterStyleAlgorithm, CounterStyleSystemStyleValue::Extends> {
|
|
|
|
|
return extends;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#descdef-counter-style-symbols
|
|
|
|
|
Vector<CounterStyleSymbol> CounterStyleDefinition::resolve_symbols(NonnullRefPtr<StyleValue const> const& symbols_style_value)
|
|
|
|
|
{
|
|
|
|
|
auto const& entries = symbols_style_value->as_value_list().values();
|
|
|
|
|
|
|
|
|
|
Vector<CounterStyleSymbol> symbols;
|
|
|
|
|
symbols.ensure_capacity(entries.size());
|
|
|
|
|
|
|
|
|
|
for (auto const& entry : entries)
|
|
|
|
|
symbols.unchecked_append(string_from_style_value(entry));
|
|
|
|
|
|
|
|
|
|
return symbols;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#descdef-counter-style-additive-symbols
|
|
|
|
|
Vector<AdditiveCounterStyleAlgorithm::AdditiveTuple> CounterStyleDefinition::resolve_additive_symbols(NonnullRefPtr<StyleValue const> const& additive_symbols_style_value, ComputationContext const& computation_context)
|
|
|
|
|
{
|
|
|
|
|
auto const& entries = additive_symbols_style_value->as_value_list().values();
|
|
|
|
|
|
|
|
|
|
Vector<AdditiveCounterStyleAlgorithm::AdditiveTuple> additive_tuples;
|
|
|
|
|
additive_tuples.ensure_capacity(entries.size());
|
|
|
|
|
|
|
|
|
|
for (auto const& entry : entries) {
|
|
|
|
|
auto const& tuple = entry->as_value_list().values();
|
|
|
|
|
VERIFY(tuple.size() == 2);
|
|
|
|
|
|
|
|
|
|
auto weight = AK::clamp_to<i32>(int_from_style_value(tuple[0]->absolutized(computation_context)));
|
|
|
|
|
auto symbol = string_from_style_value(tuple[1]);
|
|
|
|
|
|
|
|
|
|
additive_tuples.unchecked_append({ weight, symbol });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return additive_tuples;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#counter-style-negative
|
|
|
|
|
CounterStyleNegativeSign CounterStyleDefinition::resolve_negative_sign(NonnullRefPtr<StyleValue const> const& style_value)
|
|
|
|
|
{
|
|
|
|
|
auto const& negative_entries = style_value->as_value_list().values();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
.prefix = string_from_style_value(negative_entries[0]),
|
|
|
|
|
.suffix = negative_entries.size() > 1 ? string_from_style_value(negative_entries[1]) : ""_fly_string,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#counter-style-range
|
|
|
|
|
Variant<AutoRange, Vector<CounterStyleRangeEntry>> CounterStyleDefinition::resolve_range(NonnullRefPtr<StyleValue const> const& style_value, ComputationContext const& computation_context)
|
|
|
|
|
{
|
|
|
|
|
// auto
|
|
|
|
|
// NB: Resolving auto depends on the algorithm, which we may not know at parse time i.e. if the system is 'extends'
|
|
|
|
|
// To handle this we return an intermediate value which we resolve when creating the CounterStyle.
|
|
|
|
|
if (style_value->has_auto())
|
|
|
|
|
return AutoRange {};
|
|
|
|
|
|
|
|
|
|
// [ [ <integer> | infinite ]{2} ]#
|
|
|
|
|
// This defines a comma-separated list of ranges. For each individual range, the first value is the lower bound and
|
|
|
|
|
// the second value is the upper bound. This range is inclusive - it contains both the lower and upper bound
|
|
|
|
|
// numbers. If infinite is used as the first value in a range, it represents negative infinity; if used as the
|
|
|
|
|
// second value, it represents positive infinity. The range of the counter style is the union of all the ranges
|
|
|
|
|
// defined in the list.
|
|
|
|
|
auto const& range_entries = style_value->as_value_list().values();
|
|
|
|
|
|
|
|
|
|
Vector<CounterStyleRangeEntry> ranges;
|
|
|
|
|
ranges.ensure_capacity(range_entries.size());
|
|
|
|
|
|
|
|
|
|
for (auto const& entry : range_entries) {
|
|
|
|
|
auto const& range_values = entry->as_value_list().values();
|
|
|
|
|
VERIFY(range_values.size() == 2);
|
|
|
|
|
|
2026-03-26 21:33:07 +13:00
|
|
|
auto const resolve_value = [&](NonnullRefPtr<StyleValue const> const& value, i32 infinite_value) {
|
2026-02-05 22:37:32 +13:00
|
|
|
if (value->is_keyword() && value->to_keyword() == Keyword::Infinite)
|
|
|
|
|
return infinite_value;
|
|
|
|
|
|
|
|
|
|
return int_from_style_value(value->absolutized(computation_context));
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-26 21:33:07 +13:00
|
|
|
ranges.unchecked_append({ resolve_value(range_values[0], NumericLimits<i32>::min()), resolve_value(range_values[1], NumericLimits<i32>::max()) });
|
2026-02-05 22:37:32 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ranges;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#counter-style-pad
|
|
|
|
|
CounterStylePad CounterStyleDefinition::resolve_pad(NonnullRefPtr<StyleValue const> const& style_value, ComputationContext const& computation_context)
|
|
|
|
|
{
|
|
|
|
|
auto const& pad_entries = style_value->as_value_list().values();
|
|
|
|
|
|
|
|
|
|
return CounterStylePad {
|
|
|
|
|
.minimum_length = AK::clamp_to<i32>(int_from_style_value(pad_entries[0]->absolutized(computation_context))),
|
|
|
|
|
.symbol = string_from_style_value(pad_entries[1]),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|