Coverage Report

Created: 2025-03-04 07:22

/src/serenity/Userland/Libraries/LibWeb/CSS/StyleValues/CounterStyleValue.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
3
 * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
4
 * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
5
 *
6
 * SPDX-License-Identifier: BSD-2-Clause
7
 */
8
9
#include "CounterStyleValue.h"
10
#include <LibWeb/CSS/Enums.h>
11
#include <LibWeb/CSS/Keyword.h>
12
#include <LibWeb/CSS/Serialize.h>
13
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
14
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
15
#include <LibWeb/DOM/Element.h>
16
17
namespace Web::CSS {
18
19
CounterStyleValue::CounterStyleValue(CounterFunction function, FlyString counter_name, ValueComparingNonnullRefPtr<CSSStyleValue const> counter_style, FlyString join_string)
20
0
    : StyleValueWithDefaultOperators(Type::Counter)
21
0
    , m_properties {
22
0
        .function = function,
23
0
        .counter_name = move(counter_name),
24
0
        .counter_style = move(counter_style),
25
0
        .join_string = move(join_string)
26
0
    }
27
0
{
28
0
}
29
30
0
CounterStyleValue::~CounterStyleValue() = default;
31
32
// https://drafts.csswg.org/css-counter-styles-3/#generate-a-counter
33
static String generate_a_counter_representation(CSSStyleValue const& counter_style, i32 value)
34
0
{
35
    // When asked to generate a counter representation using a particular counter style for a particular
36
    // counter value, follow these steps:
37
    // TODO: 1. If the counter style is unknown, exit this algorithm and instead generate a counter representation
38
    //    using the decimal style and the same counter value.
39
    // TODO: 2. If the counter value is outside the range of the counter style, exit this algorithm and instead
40
    //    generate a counter representation using the counter style’s fallback style and the same counter value.
41
    // TODO: 3. Using the counter value and the counter algorithm for the counter style, generate an initial
42
    //    representation for the counter value.
43
    //    If the counter value is negative and the counter style uses a negative sign, instead generate an
44
    //    initial representation using the absolute value of the counter value.
45
    // TODO: 4. Prepend symbols to the representation as specified in the pad descriptor.
46
    // TODO: 5. If the counter value is negative and the counter style uses a negative sign, wrap the representation
47
    //    in the counter style’s negative sign as specified in the negative descriptor.
48
    // TODO: 6. Return the representation.
49
50
    // FIXME: Below is an ad-hoc implementation until we support @counter-style.
51
    //  It's based largely on the ListItemMarkerBox code, with minimal adjustments.
52
0
    if (counter_style.is_custom_ident()) {
53
0
        auto counter_style_name = counter_style.as_custom_ident().custom_ident();
54
0
        auto keyword = keyword_from_string(counter_style_name);
55
0
        if (keyword.has_value()) {
56
0
            auto list_style_type = keyword_to_list_style_type(*keyword);
57
0
            if (list_style_type.has_value()) {
58
0
                switch (*list_style_type) {
59
0
                case ListStyleType::Square:
60
0
                    return "▪"_string;
61
0
                case ListStyleType::Circle:
62
0
                    return "◦"_string;
63
0
                case ListStyleType::Disc:
64
0
                    return "•"_string;
65
0
                case ListStyleType::DisclosureClosed:
66
0
                    return "▸"_string;
67
0
                case ListStyleType::DisclosureOpen:
68
0
                    return "▾"_string;
69
0
                case ListStyleType::Decimal:
70
0
                    return MUST(String::formatted("{}", value));
71
0
                case ListStyleType::DecimalLeadingZero:
72
                    // This is weird, but in accordance to spec.
73
0
                    if (value < 10)
74
0
                        return MUST(String::formatted("0{}", value));
75
0
                    return MUST(String::formatted("{}", value));
76
0
                case ListStyleType::LowerAlpha:
77
0
                case ListStyleType::LowerLatin:
78
0
                    return MUST(String::from_byte_string(ByteString::bijective_base_from(value - 1).to_lowercase()));
79
0
                case ListStyleType::UpperAlpha:
80
0
                case ListStyleType::UpperLatin:
81
0
                    return MUST(String::from_byte_string(ByteString::bijective_base_from(value - 1)));
82
0
                case ListStyleType::LowerRoman:
83
0
                    return MUST(String::from_byte_string(ByteString::roman_number_from(value).to_lowercase()));
84
0
                case ListStyleType::UpperRoman:
85
0
                    return MUST(String::from_byte_string(ByteString::roman_number_from(value)));
86
0
                default:
87
0
                    break;
88
0
                }
89
0
            }
90
0
        }
91
0
    }
92
    // FIXME: Handle `symbols()` function for counter_style.
93
94
0
    dbgln("FIXME: Unsupported counter style '{}'", counter_style.to_string());
95
0
    return MUST(String::formatted("{}", value));
96
0
}
97
98
String CounterStyleValue::resolve(DOM::Element& element) const
99
0
{
100
    // "If no counter named <counter-name> exists on an element where counter() or counters() is used,
101
    // one is first instantiated with a starting value of 0."
102
0
    auto& counters_set = element.ensure_counters_set();
103
0
    if (!counters_set.last_counter_with_name(m_properties.counter_name).has_value())
104
0
        counters_set.instantiate_a_counter(m_properties.counter_name, element.unique_id(), false, 0);
105
106
    // counter( <counter-name>, <counter-style>? )
107
    // "Represents the value of the innermost counter in the element’s CSS counters set named <counter-name>
108
    // using the counter style named <counter-style>."
109
0
    if (m_properties.function == CounterFunction::Counter) {
110
        // NOTE: This should always be present because of the handling of a missing counter above.
111
0
        auto& counter = counters_set.last_counter_with_name(m_properties.counter_name).value();
112
0
        return generate_a_counter_representation(m_properties.counter_style, counter.value.value_or(0).value());
113
0
    }
114
115
    // counters( <counter-name>, <string>, <counter-style>? )
116
    // "Represents the values of all the counters in the element’s CSS counters set named <counter-name>
117
    // using the counter style named <counter-style>, sorted in outermost-first to innermost-last order
118
    // and joined by the specified <string>."
119
    // NOTE: The way counters sets are inherited, this should be the order they appear in the counters set.
120
0
    StringBuilder stb;
121
0
    for (auto const& counter : counters_set.counters()) {
122
0
        if (counter.name != m_properties.counter_name)
123
0
            continue;
124
125
0
        auto counter_string = generate_a_counter_representation(m_properties.counter_style, counter.value.value_or(0).value());
126
0
        if (!stb.is_empty())
127
0
            stb.append(m_properties.join_string);
128
0
        stb.append(counter_string);
129
0
    }
130
0
    return stb.to_string_without_validation();
131
0
}
132
133
// https://drafts.csswg.org/cssom-1/#ref-for-typedef-counter
134
String CounterStyleValue::to_string() const
135
0
{
136
    // The return value of the following algorithm:
137
    // 1. Let s be the empty string.
138
0
    StringBuilder s;
139
140
    // 2. If <counter> has three CSS component values append the string "counters(" to s.
141
0
    if (m_properties.function == CounterFunction::Counters)
142
0
        s.append("counters("sv);
143
144
    // 3. If <counter> has two CSS component values append the string "counter(" to s.
145
0
    else if (m_properties.function == CounterFunction::Counter)
146
0
        s.append("counter("sv);
147
148
    // 4. Let list be a list of CSS component values belonging to <counter>,
149
    //    omitting the last CSS component value if it is "decimal".
150
0
    Vector<RefPtr<CSSStyleValue const>> list;
151
0
    list.append(CustomIdentStyleValue::create(m_properties.counter_name));
152
0
    if (m_properties.function == CounterFunction::Counters)
153
0
        list.append(StringStyleValue::create(m_properties.join_string.to_string()));
154
0
    if (m_properties.counter_style->to_keyword() != Keyword::Decimal)
155
0
        list.append(m_properties.counter_style);
156
157
    // 5. Let each item in list be the result of invoking serialize a CSS component value on that item.
158
    // 6. Append the result of invoking serialize a comma-separated list on list to s.
159
0
    serialize_a_comma_separated_list(s, list, [](auto& builder, auto& item) {
160
0
        builder.append(item->to_string());
161
0
    });
162
163
    // 7. Append ")" (U+0029) to s.
164
0
    s.append(")"sv);
165
166
    // 8. Return s.
167
0
    return MUST(s.to_string());
168
0
}
169
170
bool CounterStyleValue::properties_equal(CounterStyleValue const& other) const
171
0
{
172
0
    return m_properties == other.m_properties;
173
0
}
174
175
}