/src/serenity/Userland/Libraries/LibWeb/HTML/DOMStringMap.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/CharacterTypes.h> |
8 | | #include <LibWeb/Bindings/DOMStringMapPrototype.h> |
9 | | #include <LibWeb/Bindings/Intrinsics.h> |
10 | | #include <LibWeb/DOM/Document.h> |
11 | | #include <LibWeb/DOM/Element.h> |
12 | | #include <LibWeb/HTML/DOMStringMap.h> |
13 | | |
14 | | namespace Web::HTML { |
15 | | |
16 | | JS_DEFINE_ALLOCATOR(DOMStringMap); |
17 | | |
18 | | JS::NonnullGCPtr<DOMStringMap> DOMStringMap::create(DOM::Element& element) |
19 | 0 | { |
20 | 0 | auto& realm = element.realm(); |
21 | 0 | return realm.heap().allocate<DOMStringMap>(realm, element); |
22 | 0 | } |
23 | | |
24 | | DOMStringMap::DOMStringMap(DOM::Element& element) |
25 | 0 | : PlatformObject(element.realm()) |
26 | 0 | , m_associated_element(element) |
27 | 0 | { |
28 | 0 | m_legacy_platform_object_flags = LegacyPlatformObjectFlags { |
29 | 0 | .supports_named_properties = true, |
30 | 0 | .has_named_property_setter = true, |
31 | 0 | .has_named_property_deleter = true, |
32 | 0 | .has_legacy_override_built_ins_interface_extended_attribute = true, |
33 | 0 | }; |
34 | 0 | } |
35 | | |
36 | 0 | DOMStringMap::~DOMStringMap() = default; |
37 | | |
38 | | void DOMStringMap::initialize(JS::Realm& realm) |
39 | 0 | { |
40 | 0 | Base::initialize(realm); |
41 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(DOMStringMap); |
42 | 0 | } |
43 | | |
44 | | void DOMStringMap::visit_edges(Cell::Visitor& visitor) |
45 | 0 | { |
46 | 0 | Base::visit_edges(visitor); |
47 | 0 | visitor.visit(m_associated_element); |
48 | 0 | } |
49 | | |
50 | | // https://html.spec.whatwg.org/multipage/dom.html#concept-domstringmap-pairs |
51 | | Vector<DOMStringMap::NameValuePair> DOMStringMap::get_name_value_pairs() const |
52 | 0 | { |
53 | | // 1. Let list be an empty list of name-value pairs. |
54 | 0 | Vector<NameValuePair> list; |
55 | | |
56 | | // 2. For each content attribute on the DOMStringMap's associated element whose first five characters are the string "data-" and whose remaining characters (if any) do not include any ASCII upper alphas, |
57 | | // in the order that those attributes are listed in the element's attribute list, add a name-value pair to list whose name is the attribute's name with the first five characters removed and whose value |
58 | | // is the attribute's value. |
59 | 0 | m_associated_element->for_each_attribute([&](auto& name, auto& value) { |
60 | 0 | if (!name.bytes_as_string_view().starts_with("data-"sv)) |
61 | 0 | return; |
62 | | |
63 | 0 | auto name_after_starting_data = name.bytes_as_string_view().substring_view(5); |
64 | |
|
65 | 0 | for (auto character : name_after_starting_data) { |
66 | 0 | if (is_ascii_upper_alpha(character)) |
67 | 0 | return; |
68 | 0 | } |
69 | | |
70 | | // 3. For each name in list, for each U+002D HYPHEN-MINUS character (-) in the name that is followed by an ASCII lower alpha, remove the U+002D HYPHEN-MINUS character (-) and replace the character |
71 | | // that followed it by the same character converted to ASCII uppercase. |
72 | 0 | StringBuilder builder; |
73 | 0 | for (size_t character_index = 0; character_index < name_after_starting_data.length(); ++character_index) { |
74 | 0 | auto current_character = name_after_starting_data[character_index]; |
75 | |
|
76 | 0 | if (character_index + 1 < name_after_starting_data.length() && current_character == '-') { |
77 | 0 | auto next_character = name_after_starting_data[character_index + 1]; |
78 | |
|
79 | 0 | if (is_ascii_lower_alpha(next_character)) { |
80 | 0 | builder.append(to_ascii_uppercase(next_character)); |
81 | | |
82 | | // Skip the next character |
83 | 0 | ++character_index; |
84 | |
|
85 | 0 | continue; |
86 | 0 | } |
87 | 0 | } |
88 | | |
89 | 0 | builder.append(current_character); |
90 | 0 | } |
91 | |
|
92 | 0 | list.append({ MUST(builder.to_string()), value }); |
93 | 0 | }); |
94 | | |
95 | | // 4. Return list. |
96 | 0 | return list; |
97 | 0 | } |
98 | | |
99 | | // https://html.spec.whatwg.org/multipage/dom.html#concept-domstringmap-pairs |
100 | | // NOTE: There isn't a direct link to this, so the link is to one of the algorithms above it. |
101 | | Vector<FlyString> DOMStringMap::supported_property_names() const |
102 | 0 | { |
103 | | // The supported property names on a DOMStringMap object at any instant are the names of each pair returned from getting the DOMStringMap's name-value pairs at that instant, in the order returned. |
104 | 0 | Vector<FlyString> names; |
105 | 0 | auto name_value_pairs = get_name_value_pairs(); |
106 | 0 | for (auto& name_value_pair : name_value_pairs) { |
107 | 0 | names.append(name_value_pair.name); |
108 | 0 | } |
109 | 0 | return names; |
110 | 0 | } |
111 | | |
112 | | // https://html.spec.whatwg.org/multipage/dom.html#dom-domstringmap-nameditem |
113 | | String DOMStringMap::determine_value_of_named_property(FlyString const& name) const |
114 | 0 | { |
115 | | // To determine the value of a named property name for a DOMStringMap, return the value component of the name-value pair whose name component is name in the list returned from getting the |
116 | | // DOMStringMap's name-value pairs. |
117 | 0 | auto name_value_pairs = get_name_value_pairs(); |
118 | 0 | auto optional_value = name_value_pairs.first_matching([&name](NameValuePair const& name_value_pair) { |
119 | 0 | return name_value_pair.name == name; |
120 | 0 | }); |
121 | | |
122 | | // NOTE: determine_value_of_named_property is only called if `name` is in supported_property_names. |
123 | 0 | VERIFY(optional_value.has_value()); |
124 | | |
125 | 0 | return optional_value->value; |
126 | 0 | } |
127 | | |
128 | | // https://html.spec.whatwg.org/multipage/dom.html#dom-domstringmap-setitem |
129 | | WebIDL::ExceptionOr<void> DOMStringMap::set_value_of_new_named_property(String const& name, JS::Value unconverted_value) |
130 | 0 | { |
131 | | // NOTE: Since PlatformObject does not know the type of value, we must convert it ourselves. |
132 | | // The type of `value` is `DOMString`. |
133 | 0 | auto value = TRY(unconverted_value.to_string(vm())); |
134 | |
|
135 | 0 | StringBuilder builder; |
136 | | |
137 | | // 3. Insert the string data- at the front of name. |
138 | | // NOTE: This is done out of order because StringBuilder doesn't have prepend. |
139 | 0 | builder.append("data-"sv); |
140 | |
|
141 | 0 | auto name_view = name.bytes_as_string_view(); |
142 | |
|
143 | 0 | for (size_t character_index = 0; character_index < name_view.length(); ++character_index) { |
144 | | // 1. If name contains a U+002D HYPHEN-MINUS character (-) followed by an ASCII lower alpha, then throw a "SyntaxError" DOMException. |
145 | 0 | auto current_character = name_view[character_index]; |
146 | |
|
147 | 0 | if (current_character == '-' && character_index + 1 < name_view.length()) { |
148 | 0 | auto next_character = name_view[character_index + 1]; |
149 | 0 | if (is_ascii_lower_alpha(next_character)) |
150 | 0 | return WebIDL::SyntaxError::create(realm(), "Name cannot contain a '-' followed by a lowercase character."_string); |
151 | 0 | } |
152 | | |
153 | | // 2. For each ASCII upper alpha in name, insert a U+002D HYPHEN-MINUS character (-) before the character and replace the character with the same character converted to ASCII lowercase. |
154 | 0 | if (is_ascii_upper_alpha(current_character)) { |
155 | 0 | builder.append('-'); |
156 | 0 | builder.append(to_ascii_lowercase(current_character)); |
157 | 0 | continue; |
158 | 0 | } |
159 | | |
160 | 0 | builder.append(current_character); |
161 | 0 | } |
162 | | |
163 | 0 | auto data_name = MUST(builder.to_string()); |
164 | | |
165 | | // FIXME: 4. If name does not match the XML Name production, throw an "InvalidCharacterError" DOMException. |
166 | | |
167 | | // 5. Set an attribute value for the DOMStringMap's associated element using name and value. |
168 | 0 | TRY(m_associated_element->set_attribute(data_name, value)); |
169 | |
|
170 | 0 | return {}; |
171 | 0 | } |
172 | | |
173 | | // https://html.spec.whatwg.org/multipage/dom.html#dom-domstringmap-setitem |
174 | | WebIDL::ExceptionOr<void> DOMStringMap::set_value_of_existing_named_property(String const& name, JS::Value value) |
175 | 0 | { |
176 | 0 | return set_value_of_new_named_property(name, value); |
177 | 0 | } |
178 | | |
179 | | // https://html.spec.whatwg.org/multipage/dom.html#dom-domstringmap-removeitem |
180 | | WebIDL::ExceptionOr<Bindings::PlatformObject::DidDeletionFail> DOMStringMap::delete_value(String const& name) |
181 | 0 | { |
182 | 0 | StringBuilder builder; |
183 | | |
184 | | // 2. Insert the string data- at the front of name. |
185 | | // NOTE: This is done out of order because StringBuilder doesn't have prepend. |
186 | 0 | builder.append("data-"sv); |
187 | |
|
188 | 0 | for (auto character : name.bytes_as_string_view()) { |
189 | | // 1. For each ASCII upper alpha in name, insert a U+002D HYPHEN-MINUS character (-) before the character and replace the character with the same character converted to ASCII lowercase. |
190 | 0 | if (is_ascii_upper_alpha(character)) { |
191 | 0 | builder.append('-'); |
192 | 0 | builder.append(to_ascii_lowercase(character)); |
193 | 0 | continue; |
194 | 0 | } |
195 | | |
196 | 0 | builder.append(character); |
197 | 0 | } |
198 | | |
199 | | // Remove an attribute by name given name and the DOMStringMap's associated element. |
200 | 0 | auto data_name = MUST(builder.to_string()); |
201 | 0 | m_associated_element->remove_attribute(data_name); |
202 | | |
203 | | // The spec doesn't have the step. This indicates that the deletion was successful. |
204 | 0 | return DidDeletionFail::No; |
205 | 0 | } |
206 | | |
207 | | JS::Value DOMStringMap::named_item_value(FlyString const& name) const |
208 | 0 | { |
209 | 0 | return JS::PrimitiveString::create(vm(), determine_value_of_named_property(name)); |
210 | 0 | } |
211 | | |
212 | | } |