summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/DOM/DOMTokenList.cpp
blob: 2934ead8ccb7c7673a6901bfd9d34ffc470a04ae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/*
 * Copyright (c) 2021, Tim Flynn <trflynn89@serenityos.org>
 * Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/CharacterTypes.h>
#include <AK/StringBuilder.h>
#include <LibWeb/DOM/DOMException.h>
#include <LibWeb/DOM/DOMTokenList.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/Window.h>

namespace {

// https://infra.spec.whatwg.org/#set-append
inline void append_to_ordered_set(Vector<String>& set, String item)
{
    if (!set.contains_slow(item))
        set.append(move(item));
}

// https://infra.spec.whatwg.org/#list-remove
inline void remove_from_ordered_set(Vector<String>& set, StringView item)
{
    set.remove_first_matching([&](auto const& value) { return value == item; });
}

// https://infra.spec.whatwg.org/#set-replace
inline void replace_in_ordered_set(Vector<String>& set, StringView item, String replacement)
{
    auto item_index = set.find_first_index(item);
    VERIFY(item_index.has_value());

    auto replacement_index = set.find_first_index(replacement);
    if (!replacement_index.has_value()) {
        set[*item_index] = move(replacement);
        return;
    }

    auto index_to_set = min(*item_index, *replacement_index);
    auto index_to_remove = max(*item_index, *replacement_index);
    if (index_to_set == index_to_remove)
        return;

    set[index_to_set] = move(replacement);
    set.remove(index_to_remove);
}

}

namespace Web::DOM {

DOMTokenList* DOMTokenList::create(Element const& associated_element, FlyString associated_attribute)
{
    auto& realm = associated_element.document().window().realm();
    return realm.heap().allocate<DOMTokenList>(realm, associated_element, move(associated_attribute));
}

// https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A2
DOMTokenList::DOMTokenList(Element const& associated_element, FlyString associated_attribute)
    : Bindings::LegacyPlatformObject(associated_element.window().cached_web_prototype("DOMTokenList"))
    , m_associated_element(associated_element)
    , m_associated_attribute(move(associated_attribute))
{
    auto value = associated_element.get_attribute(m_associated_attribute);
    associated_attribute_changed(value);
}

// https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A1
void DOMTokenList::associated_attribute_changed(StringView value)
{
    m_token_set.clear();

    if (value.is_empty())
        return;

    auto split_values = value.split_view(' ');
    for (auto const& split_value : split_values)
        append_to_ordered_set(m_token_set, split_value);
}

// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-indices%E2%91%A3
bool DOMTokenList::is_supported_property_index(u32 index) const
{
    return index < m_token_set.size();
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-item
String const& DOMTokenList::item(size_t index) const
{
    static const String null_string {};

    // 1. If index is equal to or greater than this’s token set’s size, then return null.
    if (index >= m_token_set.size())
        return null_string;

    // 2. Return this’s token set[index].
    return m_token_set[index];
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-contains
bool DOMTokenList::contains(StringView token)
{
    return m_token_set.contains_slow(token);
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-add
ExceptionOr<void> DOMTokenList::add(Vector<String> const& tokens)
{
    // 1. For each token in tokens:
    for (auto const& token : tokens) {
        // a. If token is the empty string, then throw a "SyntaxError" DOMException.
        // b. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
        TRY(validate_token(token));

        // 2. For each token in tokens, append token to this’s token set.
        append_to_ordered_set(m_token_set, token);
    }

    // 3. Run the update steps.
    run_update_steps();
    return {};
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-remove
ExceptionOr<void> DOMTokenList::remove(Vector<String> const& tokens)
{
    // 1. For each token in tokens:
    for (auto const& token : tokens) {
        // a. If token is the empty string, then throw a "SyntaxError" DOMException.
        // b. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
        TRY(validate_token(token));

        // 2. For each token in tokens, remove token from this’s token set.
        remove_from_ordered_set(m_token_set, token);
    }

    // 3. Run the update steps.
    run_update_steps();
    return {};
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-toggle
ExceptionOr<bool> DOMTokenList::toggle(String const& token, Optional<bool> force)
{
    // 1. If token is the empty string, then throw a "SyntaxError" DOMException.
    // 2. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
    TRY(validate_token(token));

    // 3. If this’s token set[token] exists, then:
    if (contains(token)) {
        // a. If force is either not given or is false, then remove token from this’s token set, run the update steps and return false.
        if (!force.has_value() || !force.value()) {
            remove_from_ordered_set(m_token_set, token);
            run_update_steps();
            return false;
        }

        // b. Return true.
        return true;
    }

    // 4. Otherwise, if force not given or is true, append token to this’s token set, run the update steps, and return true.
    if (!force.has_value() || force.value()) {
        append_to_ordered_set(m_token_set, token);
        run_update_steps();
        return true;
    }

    // 5. Return false.
    return false;
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-replace
ExceptionOr<bool> DOMTokenList::replace(String const& token, String const& new_token)
{
    // 1. If either token or newToken is the empty string, then throw a "SyntaxError" DOMException.
    // 2. If either token or newToken contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
    TRY(validate_token(token));
    TRY(validate_token(new_token));

    // 3. If this’s token set does not contain token, then return false.
    if (!contains(token))
        return false;

    // 4. Replace token in this’s token set with newToken.
    replace_in_ordered_set(m_token_set, token, new_token);

    // 5. Run the update steps.
    run_update_steps();

    // 6. Return true.
    return true;
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-supports
// https://dom.spec.whatwg.org/#concept-domtokenlist-validation
ExceptionOr<bool> DOMTokenList::supports([[maybe_unused]] StringView token)
{
    // FIXME: Implement this fully when any use case defines supported tokens.

    // 1. If the associated attribute’s local name does not define supported tokens, throw a TypeError.
    return DOM::SimpleException {
        DOM::SimpleExceptionType::TypeError,
        String::formatted("Attribute {} does not define any supported tokens", m_associated_attribute)
    };

    // 2. Let lowercase token be a copy of token, in ASCII lowercase.
    // 3. If lowercase token is present in supported tokens, return true.
    // 4. Return false.
}

// https://dom.spec.whatwg.org/#dom-domtokenlist-value
String DOMTokenList::value() const
{
    StringBuilder builder;
    builder.join(' ', m_token_set);
    return builder.build();
}

// https://dom.spec.whatwg.org/#ref-for-concept-element-attributes-set-value%E2%91%A2
void DOMTokenList::set_value(String value)
{
    JS::GCPtr<DOM::Element> associated_element = m_associated_element.ptr();
    if (!associated_element)
        return;

    associated_element->set_attribute(m_associated_attribute, move(value));
}

ExceptionOr<void> DOMTokenList::validate_token(StringView token) const
{
    if (token.is_empty())
        return SyntaxError::create("Non-empty DOM tokens are not allowed");
    if (any_of(token, is_ascii_space))
        return InvalidCharacterError::create("DOM tokens containing ASCII whitespace are not allowed");
    return {};
}

// https://dom.spec.whatwg.org/#concept-dtl-update
void DOMTokenList::run_update_steps()
{
    JS::GCPtr<DOM::Element> associated_element = m_associated_element.ptr();
    if (!associated_element)
        return;

    // 1. If the associated element does not have an associated attribute and token set is empty, then return.
    if (!associated_element->has_attribute(m_associated_attribute) && m_token_set.is_empty())
        return;

    // 2. Set an attribute value for the associated element using associated attribute’s local name and the result of running the ordered set serializer for token set.
    associated_element->set_attribute(m_associated_attribute, value());
}

JS::Value DOMTokenList::item_value(size_t index) const
{
    auto const& string = item(index);
    if (string.is_null())
        return JS::js_undefined();
    return JS::js_string(vm(), string);
}

}