Serenity Operating System
1/*
2 * Copyright (c) 2021, Tim Flynn <trflynn89@serenityos.org>
3 * Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/StringBuilder.h>
9#include <LibWeb/DOM/DOMTokenList.h>
10#include <LibWeb/DOM/Document.h>
11#include <LibWeb/DOM/Element.h>
12#include <LibWeb/Infra/CharacterTypes.h>
13#include <LibWeb/WebIDL/DOMException.h>
14
15namespace {
16
17// https://infra.spec.whatwg.org/#set-append
18inline void append_to_ordered_set(Vector<DeprecatedString>& set, DeprecatedString item)
19{
20 if (!set.contains_slow(item))
21 set.append(move(item));
22}
23
24// https://infra.spec.whatwg.org/#list-remove
25inline void remove_from_ordered_set(Vector<DeprecatedString>& set, StringView item)
26{
27 set.remove_first_matching([&](auto const& value) { return value == item; });
28}
29
30// https://infra.spec.whatwg.org/#set-replace
31inline void replace_in_ordered_set(Vector<DeprecatedString>& set, StringView item, DeprecatedString replacement)
32{
33 auto item_index = set.find_first_index(item);
34 VERIFY(item_index.has_value());
35
36 auto replacement_index = set.find_first_index(replacement);
37 if (!replacement_index.has_value()) {
38 set[*item_index] = move(replacement);
39 return;
40 }
41
42 auto index_to_set = min(*item_index, *replacement_index);
43 auto index_to_remove = max(*item_index, *replacement_index);
44 if (index_to_set == index_to_remove)
45 return;
46
47 set[index_to_set] = move(replacement);
48 set.remove(index_to_remove);
49}
50
51}
52
53namespace Web::DOM {
54
55WebIDL::ExceptionOr<JS::NonnullGCPtr<DOMTokenList>> DOMTokenList::create(Element& associated_element, DeprecatedFlyString associated_attribute)
56{
57 auto& realm = associated_element.realm();
58 return MUST_OR_THROW_OOM(realm.heap().allocate<DOMTokenList>(realm, associated_element, move(associated_attribute)));
59}
60
61// https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A2
62DOMTokenList::DOMTokenList(Element& associated_element, DeprecatedFlyString associated_attribute)
63 : Bindings::LegacyPlatformObject(associated_element.realm())
64 , m_associated_element(associated_element)
65 , m_associated_attribute(move(associated_attribute))
66{
67 auto value = associated_element.get_attribute(m_associated_attribute);
68 associated_attribute_changed(value);
69}
70
71JS::ThrowCompletionOr<void> DOMTokenList::initialize(JS::Realm& realm)
72{
73 MUST_OR_THROW_OOM(Base::initialize(realm));
74 set_prototype(&Bindings::ensure_web_prototype<Bindings::DOMTokenListPrototype>(realm, "DOMTokenList"));
75
76 return {};
77}
78
79void DOMTokenList::visit_edges(Cell::Visitor& visitor)
80{
81 Base::visit_edges(visitor);
82 visitor.visit(m_associated_element);
83}
84
85// https://dom.spec.whatwg.org/#ref-for-domtokenlist%E2%91%A0%E2%91%A1
86void DOMTokenList::associated_attribute_changed(StringView value)
87{
88 m_token_set.clear();
89
90 if (value.is_empty())
91 return;
92
93 auto split_values = value.split_view_if(Infra::is_ascii_whitespace);
94 for (auto const& split_value : split_values)
95 append_to_ordered_set(m_token_set, split_value);
96}
97
98// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-indices%E2%91%A3
99bool DOMTokenList::is_supported_property_index(u32 index) const
100{
101 return index < m_token_set.size();
102}
103
104// https://dom.spec.whatwg.org/#dom-domtokenlist-item
105DeprecatedString const& DOMTokenList::item(size_t index) const
106{
107 static const DeprecatedString null_string {};
108
109 // 1. If index is equal to or greater than this’s token set’s size, then return null.
110 if (index >= m_token_set.size())
111 return null_string;
112
113 // 2. Return this’s token set[index].
114 return m_token_set[index];
115}
116
117// https://dom.spec.whatwg.org/#dom-domtokenlist-contains
118bool DOMTokenList::contains(StringView token)
119{
120 return m_token_set.contains_slow(token);
121}
122
123// https://dom.spec.whatwg.org/#dom-domtokenlist-add
124WebIDL::ExceptionOr<void> DOMTokenList::add(Vector<DeprecatedString> const& tokens)
125{
126 // 1. For each token in tokens:
127 for (auto const& token : tokens) {
128 // a. If token is the empty string, then throw a "SyntaxError" DOMException.
129 // b. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
130 TRY(validate_token(token));
131
132 // 2. For each token in tokens, append token to this’s token set.
133 append_to_ordered_set(m_token_set, token);
134 }
135
136 // 3. Run the update steps.
137 run_update_steps();
138 return {};
139}
140
141// https://dom.spec.whatwg.org/#dom-domtokenlist-remove
142WebIDL::ExceptionOr<void> DOMTokenList::remove(Vector<DeprecatedString> const& tokens)
143{
144 // 1. For each token in tokens:
145 for (auto const& token : tokens) {
146 // a. If token is the empty string, then throw a "SyntaxError" DOMException.
147 // b. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
148 TRY(validate_token(token));
149
150 // 2. For each token in tokens, remove token from this’s token set.
151 remove_from_ordered_set(m_token_set, token);
152 }
153
154 // 3. Run the update steps.
155 run_update_steps();
156 return {};
157}
158
159// https://dom.spec.whatwg.org/#dom-domtokenlist-toggle
160WebIDL::ExceptionOr<bool> DOMTokenList::toggle(DeprecatedString const& token, Optional<bool> force)
161{
162 // 1. If token is the empty string, then throw a "SyntaxError" DOMException.
163 // 2. If token contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
164 TRY(validate_token(token));
165
166 // 3. If this’s token set[token] exists, then:
167 if (contains(token)) {
168 // 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.
169 if (!force.has_value() || !force.value()) {
170 remove_from_ordered_set(m_token_set, token);
171 run_update_steps();
172 return false;
173 }
174
175 // b. Return true.
176 return true;
177 }
178
179 // 4. Otherwise, if force not given or is true, append token to this’s token set, run the update steps, and return true.
180 if (!force.has_value() || force.value()) {
181 append_to_ordered_set(m_token_set, token);
182 run_update_steps();
183 return true;
184 }
185
186 // 5. Return false.
187 return false;
188}
189
190// https://dom.spec.whatwg.org/#dom-domtokenlist-replace
191WebIDL::ExceptionOr<bool> DOMTokenList::replace(DeprecatedString const& token, DeprecatedString const& new_token)
192{
193 // 1. If either token or newToken is the empty string, then throw a "SyntaxError" DOMException.
194 // 2. If either token or newToken contains any ASCII whitespace, then throw an "InvalidCharacterError" DOMException.
195 TRY(validate_token(token));
196 TRY(validate_token(new_token));
197
198 // 3. If this’s token set does not contain token, then return false.
199 if (!contains(token))
200 return false;
201
202 // 4. Replace token in this’s token set with newToken.
203 replace_in_ordered_set(m_token_set, token, new_token);
204
205 // 5. Run the update steps.
206 run_update_steps();
207
208 // 6. Return true.
209 return true;
210}
211
212// https://dom.spec.whatwg.org/#dom-domtokenlist-supports
213// https://dom.spec.whatwg.org/#concept-domtokenlist-validation
214WebIDL::ExceptionOr<bool> DOMTokenList::supports([[maybe_unused]] StringView token)
215{
216 // FIXME: Implement this fully when any use case defines supported tokens.
217
218 // 1. If the associated attribute’s local name does not define supported tokens, throw a TypeError.
219 return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("Attribute {} does not define any supported tokens", m_associated_attribute).release_value_but_fixme_should_propagate_errors() };
220
221 // 2. Let lowercase token be a copy of token, in ASCII lowercase.
222 // 3. If lowercase token is present in supported tokens, return true.
223 // 4. Return false.
224}
225
226// https://dom.spec.whatwg.org/#dom-domtokenlist-value
227DeprecatedString DOMTokenList::value() const
228{
229 StringBuilder builder;
230 builder.join(' ', m_token_set);
231 return builder.to_deprecated_string();
232}
233
234// https://dom.spec.whatwg.org/#ref-for-concept-element-attributes-set-value%E2%91%A2
235void DOMTokenList::set_value(DeprecatedString value)
236{
237 JS::GCPtr<DOM::Element> associated_element = m_associated_element.ptr();
238 if (!associated_element)
239 return;
240
241 MUST(associated_element->set_attribute(m_associated_attribute, move(value)));
242}
243
244WebIDL::ExceptionOr<void> DOMTokenList::validate_token(StringView token) const
245{
246 if (token.is_empty())
247 return WebIDL::SyntaxError::create(realm(), "Non-empty DOM tokens are not allowed");
248 if (any_of(token, Infra::is_ascii_whitespace))
249 return WebIDL::InvalidCharacterError::create(realm(), "DOM tokens containing ASCII whitespace are not allowed");
250 return {};
251}
252
253// https://dom.spec.whatwg.org/#concept-dtl-update
254void DOMTokenList::run_update_steps()
255{
256 JS::GCPtr<DOM::Element> associated_element = m_associated_element.ptr();
257 if (!associated_element)
258 return;
259
260 // 1. If the associated element does not have an associated attribute and token set is empty, then return.
261 if (!associated_element->has_attribute(m_associated_attribute) && m_token_set.is_empty())
262 return;
263
264 // 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.
265 MUST(associated_element->set_attribute(m_associated_attribute, value()));
266}
267
268WebIDL::ExceptionOr<JS::Value> DOMTokenList::item_value(size_t index) const
269{
270 auto const& string = item(index);
271 if (string.is_null())
272 return JS::js_undefined();
273 return JS::PrimitiveString::create(vm(), string);
274}
275
276}