/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 | | } |