LibWeb: Support symbols() function in <counter-style>

This commit is contained in:
Callum Law
2026-02-27 15:36:12 +13:00
committed by Sam Atkins
parent f6eccc629c
commit 81cb968beb
Notes: github-actions[bot] 2026-02-27 16:27:03 +00:00
11 changed files with 344 additions and 26 deletions

View File

@@ -965,6 +965,13 @@
"round",
"bevel"
],
"symbols-type": [
"cyclic",
"numeric",
"alphabetic",
"symbolic",
"fixed"
],
"table-layout": [
"auto",
"fixed"

View File

@@ -2713,15 +2713,61 @@ Optional<FlyString> Parser::parse_counter_style_name(TokenStream<ComponentValue>
RefPtr<StyleValue const> Parser::parse_counter_style_value(TokenStream<ComponentValue>& tokens)
{
// <counter-style> = <counter-style-name> | <symbols()>
// <symbols()> = symbols( <symbols-type>? [ <string> | <image> ]+ )
// <symbols-type> = cyclic | numeric | alphabetic | symbolic | fixed
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// <counter-style-name>
if (auto const& counter_style_name = parse_counter_style_name(tokens); counter_style_name.has_value()) {
transaction.commit();
return CounterStyleStyleValue::create(counter_style_name.value());
}
// FIXME: Support <symbols()>
// <symbols()>
auto const& maybe_function_token = tokens.consume_a_token();
if (maybe_function_token.is_function("symbols"sv)) {
TokenStream argument_tokens { maybe_function_token.function().value };
// <symbols-type>?
// NB: <symbols-type> defaults to symbolic if not provided.
SymbolsType symbols_type = SymbolsType::Symbolic;
if (auto keyword = parse_keyword_value(argument_tokens); keyword) {
auto maybe_symbols_type = keyword_to_symbols_type(keyword->to_keyword());
if (!maybe_symbols_type.has_value())
return nullptr;
symbols_type = maybe_symbols_type.value();
}
// [ <string> | <image> ]+
// FIXME: In line with <symbol> we don't support <image> here - we may need to revisit this if other browsers
// implement it.
Vector<FlyString> symbols;
while (argument_tokens.has_next_token()) {
auto maybe_string = parse_string_value(argument_tokens);
if (!maybe_string)
break;
symbols.append(maybe_string->string_value());
}
argument_tokens.discard_whitespace();
if (argument_tokens.has_next_token())
return nullptr;
// https://drafts.csswg.org/css-counter-styles-3/#symbols-function
// If the system is alphabetic or numeric, there must be at least two <string>s or <image>s, or else the function is invalid.
if (symbols.is_empty() || (first_is_one_of(symbols_type, SymbolsType::Alphabetic, SymbolsType::Numeric) && symbols.size() < 2))
return nullptr;
transaction.commit();
return CounterStyleStyleValue::create(CounterStyleStyleValue::SymbolsFunction { symbols_type, move(symbols) });
}
return nullptr;
}

View File

@@ -7,18 +7,79 @@
#include "CounterStyleStyleValue.h"
#include <LibWeb/CSS/CounterStyle.h>
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/CSS/Serialize.h>
namespace Web::CSS {
void CounterStyleStyleValue::serialize(StringBuilder& builder, SerializationMode) const
{
builder.append(m_name);
m_value.visit(
[&](FlyString const& name) {
builder.append(name);
},
[&](SymbolsFunction const& symbols_function) {
builder.append("symbols("sv);
if (symbols_function.type != SymbolsType::Symbolic)
builder.appendff("{} ", CSS::to_string(symbols_function.type));
for (size_t i = 0; i < symbols_function.symbols.size(); ++i) {
if (i > 0)
builder.append(' ');
serialize_a_string(builder, symbols_function.symbols[i]);
}
builder.append(')');
});
}
RefPtr<CounterStyle const> CounterStyleStyleValue::resolve_counter_style(HashMap<FlyString, NonnullRefPtr<CounterStyle const>> const& registered_counter_styles) const
{
// FIXME: Support symbols() function for anonymous counter style
return registered_counter_styles.get(m_name).value_or(nullptr);
return m_value.visit(
[&](FlyString const& name) -> RefPtr<CounterStyle const> {
return registered_counter_styles.get(name).value_or(nullptr);
},
[&](SymbolsFunction const& symbols_function) -> RefPtr<CounterStyle const> {
// https://drafts.csswg.org/css-counter-styles-3/#symbols-function
auto algorithm = [&]() -> CounterStyleAlgorithm {
// The counter styles algorithm is constructed by consulting the previous chapter using the provided
// system — or symbolic if the system was omitted — and the provided <string>s and <image>s as the value
// of the symbols property. If the system is fixed, the first symbol value is 1.
switch (symbols_function.type) {
case SymbolsType::Cyclic:
return GenericCounterStyleAlgorithm { CounterStyleSystem::Cyclic, symbols_function.symbols };
case SymbolsType::Numeric:
return GenericCounterStyleAlgorithm { CounterStyleSystem::Numeric, symbols_function.symbols };
case SymbolsType::Alphabetic:
return GenericCounterStyleAlgorithm { CounterStyleSystem::Alphabetic, symbols_function.symbols };
case SymbolsType::Symbolic:
return GenericCounterStyleAlgorithm { CounterStyleSystem::Symbolic, symbols_function.symbols };
case SymbolsType::Fixed:
return FixedCounterStyleAlgorithm { .first_symbol = 1, .symbol_list = symbols_function.symbols };
}
VERIFY_NOT_REACHED();
}();
// The symbols() function defines an anonymous counter style with no name, a prefix of "" (empty string) and
// suffix of " " (U+0020 SPACE), a range of auto, a fallback of decimal, a negative of "\2D" ("-"
// hyphen-minus), a pad of 0 "", and a speak-as of auto.
// FIXME: Pass the correct speak-as value once we support that.
auto definition = CounterStyleDefinition::create(
// NB: We use empty string instead of no name for simplicity - this doesn't clash with
// <counter-style-name> since that is a <custom-ident> which can't be an empty string.
""_fly_string,
algorithm,
CounterStyleNegativeSign { "-"_fly_string, ""_fly_string },
""_fly_string,
" "_fly_string,
AutoRange {},
"decimal"_fly_string,
CounterStylePad { 0, ""_fly_string });
// NB: We don't need to pass registered counter styles here since we don't rely on extension.
return CounterStyle::from_counter_style_definition(definition, {});
});
}
}

View File

@@ -13,9 +13,16 @@ namespace Web::CSS {
class CounterStyleStyleValue : public StyleValueWithDefaultOperators<CounterStyleStyleValue> {
public:
static ValueComparingNonnullRefPtr<CounterStyleStyleValue const> create(FlyString name)
struct SymbolsFunction {
SymbolsType type;
Vector<FlyString> symbols;
bool operator==(SymbolsFunction const& other) const = default;
};
static ValueComparingNonnullRefPtr<CounterStyleStyleValue const> create(Variant<FlyString, SymbolsFunction> value)
{
return adopt_ref(*new (nothrow) CounterStyleStyleValue(move(name)));
return adopt_ref(*new (nothrow) CounterStyleStyleValue(move(value)));
}
virtual ~CounterStyleStyleValue() override = default;
@@ -24,16 +31,16 @@ public:
RefPtr<CounterStyle const> resolve_counter_style(HashMap<FlyString, NonnullRefPtr<CounterStyle const>> const& registered_counter_styles) const;
bool properties_equal(CounterStyleStyleValue const& other) const { return m_name == other.m_name; }
bool properties_equal(CounterStyleStyleValue const& other) const { return m_value == other.m_value; }
private:
explicit CounterStyleStyleValue(FlyString name)
explicit CounterStyleStyleValue(Variant<FlyString, SymbolsFunction> value)
: StyleValueWithDefaultOperators(Type::CounterStyle)
, m_name(move(name))
, m_value(move(value))
{
}
FlyString m_name;
Variant<FlyString, SymbolsFunction> m_value;
};
}

View File

@@ -467,6 +467,7 @@ enum class Scroller : u8;
enum class StepPosition : u8;
enum class StrokeLinecap : u8;
enum class StrokeLinejoin : u8;
enum class SymbolsType : u8;
enum class TextRendering : u8;
enum class TextUnderlinePositionHorizontal : u8;
enum class TextUnderlinePositionVertical : u8;

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<title>CSS Reference: symbols function, invalid</title>
<link rel="author" title="Xidorn Quan" href="https://www.upsuper.org/">
<link rel="stylesheet" href="../../../../../input/wpt-import/css/css-counter-styles/counter-style-at-rule/support/test-common.css">
<style type="text/css">
.invalid {
list-style-type: lower-greek;
}
</style>
<ol start="-2" class="invalid">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<title>CSS Reference: symbols function, invalid</title>
<link rel="author" title="Xidorn Quan" href="https://www.upsuper.org/">
<link rel="stylesheet" href="../../../../../input/wpt-import/css/css-counter-styles/counter-style-at-rule/support/test-common.css">
<style type="text/css">
@counter-style cyclic {
system: cyclic;
symbols: '*' '\2020' '\2021' '\A7';
suffix: ' ';
}
@counter-style numeric {
system: numeric;
symbols: '0' '1' '2';
suffix: ' ';
}
@counter-style alphabetic {
system: alphabetic;
symbols: '\26AA' '\26AB';
suffix: ' ';
}
@counter-style symbolic {
system: symbolic;
symbols: '*' '\2020' '\2021' '\A7';
suffix: ' ';
}
@counter-style fixed {
system: fixed;
symbols: '\25F0' '\25F1' '\25F2' '\25F3';
suffix: ' ';
}
@counter-style counter {
symbols: '*';
}
@counter-style counters {
system: numeric;
symbols: '0' '1';
}
.counter { counter-reset: a; }
.counter p { counter-increment: a 1; }
.counter p::after {
content: counter(a, counter);
}
.counter, .counters {
list-style-type: none;
counter-reset: a;
}
.counter li, .counters li {
counter-increment: a;
padding-right: .5em;
}
.counter li::after {
content: counter(a, counter);
}
.counters .counters li::after {
content: counters(a, '.', counters);
}
</style>
<ol start="-2" style="list-style-type: symbolic">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" style="list-style-type: cyclic">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" style="list-style-type: numeric">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" style="list-style-type: alphabetic">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" style="list-style-type: symbolic">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" style="list-style-type: fixed">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol class="counter">
<li><li><li><li><li>
</ol>
<ol class="counters">
<li><ol class="counters"><li><li><li><li><li></ol></li>
<li><ol class="counters"><li><li><li><li><li></ol></li>
</ol>

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<title>CSS Test: symbols function, invalid</title>
<link rel="author" title="Xidorn Quan" href="https://www.upsuper.org/">
<link rel="help" href="https://drafts.csswg.org/css-counter-styles-3/#symbols-function">
<link rel="match" href="../../../../../expected/wpt-import/css/css-counter-styles/counter-style-at-rule/symbols-function-invalid-ref.html">
<link rel="stylesheet" href="support/test-common.css">
<style type="text/css">
.invalid {
list-style-type: lower-greek;
list-style-type: symbols(a b c);
list-style-type: symbols(alphabetic a b c);
list-style-type: symbols(numeric 0 1 2);
list-style-type: symbols(additive 'a' 'b');
list-style-type: symbols(fixed);
list-style-type: symbols(alphabetic 'a');
list-style-type: symbols(numeric '0');
}
</style>
<ol start="-2" class="invalid">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<title>CSS Test: symbols function</title>
<link rel="author" title="Xidorn Quan" href="https://www.upsuper.org/">
<link rel="help" href="https://drafts.csswg.org/css-counter-styles-3/#symbols-function">
<link rel="match" href="../../../../../expected/wpt-import/css/css-counter-styles/counter-style-at-rule/symbols-function-ref.html">
<link rel="stylesheet" href="support/test-common.css">
<style type="text/css">
.default {
list-style-type: symbols('*' '\2020' '\2021' '\A7');
}
.cyclic {
list-style-type: symbols(cyclic '*' '\2020' '\2021' '\A7');
}
.numeric {
list-style-type: symbols(numeric '0' '1' '2');
}
.alphabetic {
list-style-type: symbols(alphabetic '\26AA' '\26AB');
}
.symbolic {
list-style-type: symbols(symbolic '*' '\2020' '\2021' '\A7');
}
.fixed {
list-style-type: symbols(fixed '\25F0' '\25F1' '\25F2' '\25F3');
}
.counter, .counters {
list-style-type: none;
counter-reset: a;
}
.counter li, .counters li {
counter-increment: a;
padding-right: .5em;
}
.counter li::after {
content: counter(a, symbols('*'));
}
.counters .counters li::after {
content: counters(a, '.', symbols(numeric '0' '1'));
}
</style>
<ol start="-2" class="default">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" class="cyclic">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" class="numeric">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" class="alphabetic">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" class="symbolic">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol start="-2" class="fixed">
<li><li><li><li><li>
<li><li><li><li><li>
</ol>
<ol class="counter">
<li><li><li><li><li>
</ol>
<ol class="counters">
<li><ol class="counters"><li><li><li><li><li></ol></li>
<li><ol class="counters"><li><li><li><li><li></ol></li>
</ol>

View File

@@ -2,8 +2,7 @@ Harness status: OK
Found 27 tests
21 Pass
6 Fail
27 Pass
Pass Property list-style-type value 'none'
Pass Property list-style-type value 'disc'
Pass Property list-style-type value 'circle'
@@ -25,9 +24,9 @@ Pass Property list-style-type value '"marker string"'
Pass Property list-style-type value '"Note: "'
Pass Property list-style-type value 'counter-Style-Name'
Pass Property list-style-type value 'CounterStyleName'
Fail Property list-style-type value 'symbols(cyclic "string")'
Fail Property list-style-type value 'symbols(cyclic "○" "●")'
Fail Property list-style-type value 'symbols(fixed "1")'
Fail Property list-style-type value 'symbols("string")'
Fail Property list-style-type value 'symbols(alphabetic "first" "second")'
Fail Property list-style-type value 'symbols(numeric "first" "second")'
Pass Property list-style-type value 'symbols(cyclic "string")'
Pass Property list-style-type value 'symbols(cyclic "○" "●")'
Pass Property list-style-type value 'symbols(fixed "1")'
Pass Property list-style-type value 'symbols("string")'
Pass Property list-style-type value 'symbols(alphabetic "first" "second")'
Pass Property list-style-type value 'symbols(numeric "first" "second")'

View File

@@ -2,8 +2,7 @@ Harness status: OK
Found 27 tests
21 Pass
6 Fail
27 Pass
Pass e.style['list-style-type'] = "none" should set the property value
Pass e.style['list-style-type'] = "disc" should set the property value
Pass e.style['list-style-type'] = "circle" should set the property value
@@ -25,9 +24,9 @@ Pass e.style['list-style-type'] = "\"marker string\"" should set the property va
Pass e.style['list-style-type'] = "\"Note: \"" should set the property value
Pass e.style['list-style-type'] = "counter-Style-Name" should set the property value
Pass e.style['list-style-type'] = "CounterStyleName" should set the property value
Fail e.style['list-style-type'] = "symbols(cyclic \"string\")" should set the property value
Fail e.style['list-style-type'] = "symbols(cyclic \"○\" \"●\")" should set the property value
Fail e.style['list-style-type'] = "symbols(fixed \"1\")" should set the property value
Fail e.style['list-style-type'] = "symbols(symbolic \"string\")" should set the property value
Fail e.style['list-style-type'] = "symbols(alphabetic \"first\" \"second\")" should set the property value
Fail e.style['list-style-type'] = "symbols(numeric \"first\" \"second\")" should set the property value
Pass e.style['list-style-type'] = "symbols(cyclic \"string\")" should set the property value
Pass e.style['list-style-type'] = "symbols(cyclic \"○\" \"●\")" should set the property value
Pass e.style['list-style-type'] = "symbols(fixed \"1\")" should set the property value
Pass e.style['list-style-type'] = "symbols(symbolic \"string\")" should set the property value
Pass e.style['list-style-type'] = "symbols(alphabetic \"first\" \"second\")" should set the property value
Pass e.style['list-style-type'] = "symbols(numeric \"first\" \"second\")" should set the property value