Coverage Report

Created: 2025-08-28 06:26

/src/serenity/Userland/Libraries/LibWeb/DOM/NamedNodeMap.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2021, Tim Flynn <trflynn89@serenityos.org>
3
 * Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
4
 * Copyright (c) 2022, Alexander Narsudinov <a.narsudinov@gmail.com>
5
 *
6
 * SPDX-License-Identifier: BSD-2-Clause
7
 */
8
9
#include <LibWeb/Bindings/NamedNodeMapPrototype.h>
10
#include <LibWeb/DOM/Attr.h>
11
#include <LibWeb/DOM/Document.h>
12
#include <LibWeb/DOM/NamedNodeMap.h>
13
#include <LibWeb/Infra/Strings.h>
14
#include <LibWeb/Namespace.h>
15
16
namespace Web::DOM {
17
18
JS_DEFINE_ALLOCATOR(NamedNodeMap);
19
20
JS::NonnullGCPtr<NamedNodeMap> NamedNodeMap::create(Element& element)
21
0
{
22
0
    auto& realm = element.realm();
23
0
    return realm.heap().allocate<NamedNodeMap>(realm, element);
24
0
}
25
26
NamedNodeMap::NamedNodeMap(Element& element)
27
0
    : Bindings::PlatformObject(element.realm())
28
0
    , m_element(element)
29
0
{
30
0
    m_legacy_platform_object_flags = LegacyPlatformObjectFlags {
31
0
        .supports_indexed_properties = true,
32
0
        .supports_named_properties = true,
33
0
        .has_legacy_unenumerable_named_properties_interface_extended_attribute = true,
34
0
    };
35
0
}
36
37
void NamedNodeMap::initialize(JS::Realm& realm)
38
0
{
39
0
    Base::initialize(realm);
40
0
    WEB_SET_PROTOTYPE_FOR_INTERFACE(NamedNodeMap);
41
0
}
42
43
void NamedNodeMap::visit_edges(Cell::Visitor& visitor)
44
0
{
45
0
    Base::visit_edges(visitor);
46
0
    visitor.visit(m_element);
47
0
    visitor.visit(m_attributes);
48
0
}
49
50
// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names%E2%91%A0
51
Vector<FlyString> NamedNodeMap::supported_property_names() const
52
0
{
53
    // 1. Let names be the qualified names of the attributes in this NamedNodeMap object’s attribute list, with duplicates omitted, in order.
54
0
    Vector<FlyString> names;
55
0
    names.ensure_capacity(m_attributes.size());
56
57
0
    for (auto const& attribute : m_attributes) {
58
0
        auto const attribute_name = attribute->name();
59
0
        if (!names.contains_slow(attribute_name))
60
0
            names.append(attribute_name.to_string());
61
0
    }
62
63
    // 2. If this NamedNodeMap object’s element is in the HTML namespace and its node document is an HTML document, then for each name in names:
64
    // FIXME: Handle the second condition, assume it is an HTML document for now.
65
0
    if (associated_element().namespace_uri() == Namespace::HTML) {
66
        // 1. Let lowercaseName be name, in ASCII lowercase.
67
        // 2. If lowercaseName is not equal to name, remove name from names.
68
0
        names.remove_all_matching([](auto const& name) { return name != name.to_ascii_lowercase(); });
69
0
    }
70
71
    // 3. Return names.
72
0
    return names;
73
0
}
74
75
// https://dom.spec.whatwg.org/#dom-namednodemap-item
76
Attr const* NamedNodeMap::item(u32 index) const
77
0
{
78
    // 1. If index is equal to or greater than this’s attribute list’s size, then return null.
79
0
    if (index >= m_attributes.size())
80
0
        return nullptr;
81
82
    // 2. Otherwise, return this’s attribute list[index].
83
0
    return m_attributes[index].ptr();
84
0
}
85
86
// https://dom.spec.whatwg.org/#dom-namednodemap-getnameditem
87
Attr const* NamedNodeMap::get_named_item(FlyString const& qualified_name) const
88
0
{
89
0
    return get_attribute(qualified_name);
90
0
}
91
92
// https://dom.spec.whatwg.org/#dom-namednodemap-getnameditemns
93
Attr const* NamedNodeMap::get_named_item_ns(Optional<FlyString> const& namespace_, FlyString const& local_name) const
94
0
{
95
0
    return get_attribute_ns(namespace_, local_name);
96
0
}
97
98
// https://dom.spec.whatwg.org/#dom-namednodemap-setnameditem
99
WebIDL::ExceptionOr<JS::GCPtr<Attr>> NamedNodeMap::set_named_item(Attr& attribute)
100
0
{
101
0
    return set_attribute(attribute);
102
0
}
103
104
// https://dom.spec.whatwg.org/#dom-namednodemap-setnameditemns
105
WebIDL::ExceptionOr<JS::GCPtr<Attr>> NamedNodeMap::set_named_item_ns(Attr& attribute)
106
0
{
107
0
    return set_attribute(attribute);
108
0
}
109
110
// https://dom.spec.whatwg.org/#dom-namednodemap-removenameditem
111
WebIDL::ExceptionOr<Attr const*> NamedNodeMap::remove_named_item(FlyString const& qualified_name)
112
0
{
113
    // 1. Let attr be the result of removing an attribute given qualifiedName and element.
114
0
    auto const* attribute = remove_attribute(qualified_name);
115
116
    // 2. If attr is null, then throw a "NotFoundError" DOMException.
117
0
    if (!attribute)
118
0
        return WebIDL::NotFoundError::create(realm(), MUST(String::formatted("Attribute with name '{}' not found", qualified_name)));
119
120
    // 3. Return attr.
121
0
    return attribute;
122
0
}
123
124
// https://dom.spec.whatwg.org/#dom-namednodemap-removenameditemns
125
WebIDL::ExceptionOr<Attr const*> NamedNodeMap::remove_named_item_ns(Optional<FlyString> const& namespace_, FlyString const& local_name)
126
0
{
127
    // 1. Let attr be the result of removing an attribute given namespace, localName, and element.
128
0
    auto const* attribute = remove_attribute_ns(namespace_, local_name);
129
130
    // 2. If attr is null, then throw a "NotFoundError" DOMException.
131
0
    if (!attribute)
132
0
        return WebIDL::NotFoundError::create(realm(), MUST(String::formatted("Attribute with namespace '{}' and local name '{}' not found", namespace_, local_name)));
133
134
    // 3. Return attr.
135
0
    return attribute;
136
0
}
137
138
// https://dom.spec.whatwg.org/#concept-element-attributes-get-by-name
139
Attr* NamedNodeMap::get_attribute(FlyString const& qualified_name, size_t* item_index)
140
0
{
141
0
    return const_cast<Attr*>(const_cast<NamedNodeMap const*>(this)->get_attribute(qualified_name, item_index));
142
0
}
143
144
// https://dom.spec.whatwg.org/#concept-element-attributes-get-by-name
145
Attr const* NamedNodeMap::get_attribute(FlyString const& qualified_name, size_t* item_index) const
146
0
{
147
0
    if (item_index)
148
0
        *item_index = 0;
149
150
    // 1. If element is in the HTML namespace and its node document is an HTML document, then set qualifiedName to qualifiedName in ASCII lowercase.
151
    // FIXME: Handle the second condition, assume it is an HTML document for now.
152
0
    bool compare_as_lowercase = associated_element().namespace_uri() == Namespace::HTML;
153
154
    // 2. Return the first attribute in element’s attribute list whose qualified name is qualifiedName; otherwise null.
155
0
    for (auto const& attribute : m_attributes) {
156
0
        if (compare_as_lowercase) {
157
0
            if (attribute->name().equals_ignoring_ascii_case(qualified_name))
158
0
                return attribute;
159
0
        } else {
160
0
            if (attribute->name() == qualified_name)
161
0
                return attribute;
162
0
        }
163
164
0
        if (item_index)
165
0
            ++(*item_index);
166
0
    }
167
168
0
    return nullptr;
169
0
}
170
171
Attr const* NamedNodeMap::get_attribute_with_lowercase_qualified_name(FlyString const& lowercase_qualified_name) const
172
0
{
173
0
    bool compare_as_lowercase = associated_element().namespace_uri() == Namespace::HTML;
174
0
    VERIFY(compare_as_lowercase);
175
176
0
    for (auto const& attribute : m_attributes) {
177
0
        if (attribute->lowercase_name() == lowercase_qualified_name)
178
0
            return attribute;
179
0
    }
180
181
0
    return nullptr;
182
0
}
183
184
// https://dom.spec.whatwg.org/#concept-element-attributes-get-by-namespace
185
Attr* NamedNodeMap::get_attribute_ns(Optional<FlyString> const& namespace_, FlyString const& local_name, size_t* item_index)
186
0
{
187
0
    return const_cast<Attr*>(const_cast<NamedNodeMap const*>(this)->get_attribute_ns(namespace_, local_name, item_index));
188
0
}
189
190
// https://dom.spec.whatwg.org/#concept-element-attributes-get-by-namespace
191
Attr const* NamedNodeMap::get_attribute_ns(Optional<FlyString> const& namespace_, FlyString const& local_name, size_t* item_index) const
192
0
{
193
0
    if (item_index)
194
0
        *item_index = 0;
195
196
    // 1. If namespace is the empty string, then set it to null.
197
0
    Optional<FlyString> normalized_namespace;
198
0
    if (namespace_ != String {})
199
0
        normalized_namespace = namespace_;
200
201
    // 2. Return the attribute in element’s attribute list whose namespace is namespace and local name is localName, if any; otherwise null.
202
0
    for (auto const& attribute : m_attributes) {
203
0
        if (attribute->namespace_uri() == normalized_namespace && attribute->local_name() == local_name)
204
0
            return attribute.ptr();
205
0
        if (item_index)
206
0
            ++(*item_index);
207
0
    }
208
209
0
    return nullptr;
210
0
}
211
212
// https://dom.spec.whatwg.org/#concept-element-attributes-set
213
WebIDL::ExceptionOr<JS::GCPtr<Attr>> NamedNodeMap::set_attribute(Attr& attribute)
214
0
{
215
    // 1. If attr’s element is neither null nor element, throw an "InUseAttributeError" DOMException.
216
0
    if ((attribute.owner_element() != nullptr) && (attribute.owner_element() != &associated_element()))
217
0
        return WebIDL::InUseAttributeError::create(realm(), "Attribute must not already be in use"_string);
218
219
    // 2. Let oldAttr be the result of getting an attribute given attr’s namespace, attr’s local name, and element.
220
0
    size_t old_attribute_index = 0;
221
0
    auto* old_attribute = get_attribute_ns(attribute.namespace_uri(), attribute.local_name(), &old_attribute_index);
222
223
    // 3. If oldAttr is attr, return attr.
224
0
    if (old_attribute == &attribute)
225
0
        return &attribute;
226
227
    // 4. If oldAttr is non-null, then replace oldAttr with attr.
228
0
    if (old_attribute) {
229
0
        replace_attribute(*old_attribute, attribute, old_attribute_index);
230
0
    }
231
    // 5. Otherwise, append attr to element.
232
0
    else {
233
0
        append_attribute(attribute);
234
0
    }
235
236
    // 6. Return oldAttr.
237
0
    return old_attribute;
238
0
}
239
240
// https://dom.spec.whatwg.org/#concept-element-attributes-replace
241
void NamedNodeMap::replace_attribute(Attr& old_attribute, Attr& new_attribute, size_t old_attribute_index)
242
0
{
243
0
    VERIFY(old_attribute.owner_element());
244
245
    // 1. Replace oldAttr by newAttr in oldAttr’s element’s attribute list.
246
0
    m_attributes.remove(old_attribute_index);
247
0
    m_attributes.insert(old_attribute_index, new_attribute);
248
249
    // 2. Set newAttr’s element to oldAttr’s element.
250
0
    new_attribute.set_owner_element(old_attribute.owner_element());
251
252
    // 3. Set oldAttr’s element to null.
253
0
    old_attribute.set_owner_element(nullptr);
254
255
    // 4. Handle attribute changes for oldAttr with newAttr’s element, oldAttr’s value, and newAttr’s value.
256
0
    old_attribute.handle_attribute_changes(*new_attribute.owner_element(), old_attribute.value(), new_attribute.value());
257
0
}
258
259
// https://dom.spec.whatwg.org/#concept-element-attributes-append
260
void NamedNodeMap::append_attribute(Attr& attribute)
261
0
{
262
    // 1. Append attribute to element’s attribute list.
263
0
    m_attributes.append(attribute);
264
265
    // 2. Set attribute’s element to element.
266
0
    attribute.set_owner_element(&associated_element());
267
268
    // 3. Handle attribute changes for attribute with element, null, and attribute’s value.
269
0
    attribute.handle_attribute_changes(associated_element(), {}, attribute.value());
270
0
}
271
272
// https://dom.spec.whatwg.org/#concept-element-attributes-remove
273
void NamedNodeMap::remove_attribute_at_index(size_t attribute_index)
274
0
{
275
0
    JS::NonnullGCPtr<Attr> attribute = m_attributes.at(attribute_index);
276
277
    // 1. Let element be attribute’s element.
278
0
    auto* element = attribute->owner_element();
279
0
    VERIFY(element);
280
281
    // 2. Remove attribute from element’s attribute list.
282
0
    m_attributes.remove(attribute_index);
283
284
    // 3. Set attribute’s element to null.
285
0
    attribute->set_owner_element(nullptr);
286
287
    // 4. Handle attribute changes for attribute with element, attribute’s value, and null.
288
0
    attribute->handle_attribute_changes(*element, attribute->value(), {});
289
0
}
290
291
// https://dom.spec.whatwg.org/#concept-element-attributes-remove-by-name
292
Attr const* NamedNodeMap::remove_attribute(FlyString const& qualified_name)
293
0
{
294
0
    size_t item_index = 0;
295
296
    // 1. Let attr be the result of getting an attribute given qualifiedName and element.
297
0
    auto const* attribute = get_attribute(qualified_name, &item_index);
298
299
    // 2. If attr is non-null, then remove attr.
300
0
    if (attribute)
301
0
        remove_attribute_at_index(item_index);
302
303
    // 3. Return attr.
304
0
    return attribute;
305
0
}
306
307
// https://dom.spec.whatwg.org/#concept-element-attributes-remove-by-namespace
308
Attr const* NamedNodeMap::remove_attribute_ns(Optional<FlyString> const& namespace_, FlyString const& local_name)
309
0
{
310
0
    size_t item_index = 0;
311
312
    // 1. Let attr be the result of getting an attribute given namespace, localName, and element.
313
0
    auto const* attribute = get_attribute_ns(namespace_, local_name, &item_index);
314
315
    // 2. If attr is non-null, then remove attr.
316
0
    if (attribute)
317
0
        remove_attribute_at_index(item_index);
318
319
    // 3. Return attr.
320
0
    return attribute;
321
0
}
322
323
Optional<JS::Value> NamedNodeMap::item_value(size_t index) const
324
0
{
325
0
    auto const* node = item(index);
326
0
    if (!node)
327
0
        return {};
328
0
    return node;
329
0
}
330
331
JS::Value NamedNodeMap::named_item_value(FlyString const& name) const
332
0
{
333
0
    auto const* node = get_named_item(name);
334
0
    if (!node)
335
0
        return JS::js_undefined();
336
0
    return node;
337
0
}
338
339
// https://dom.spec.whatwg.org/#dom-element-removeattributenode
340
WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> NamedNodeMap::remove_attribute_node(JS::NonnullGCPtr<Attr> attr)
341
0
{
342
    // 1. If this’s attribute list does not contain attr, then throw a "NotFoundError" DOMException.
343
0
    auto index = m_attributes.find_first_index(attr);
344
0
    if (!index.has_value())
345
0
        return WebIDL::NotFoundError::create(realm(), "Attribute not found"_string);
346
347
    // 2. Remove attr.
348
0
    remove_attribute_at_index(index.value());
349
350
    // 3. Return attr.
351
0
    return attr;
352
0
}
353
354
}