script/dom/domstringmap.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use dom_struct::dom_struct;
6use html5ever::{LocalName, ns};
7use js::context::NoGC;
8use script_bindings::reflector::{Reflector, reflect_dom_object};
9
10use crate::dom::bindings::codegen::Bindings::DOMStringMapBinding::DOMStringMapMethods;
11use crate::dom::bindings::error::{Error, ErrorResult};
12use crate::dom::bindings::inheritance::Castable;
13use crate::dom::bindings::root::{Dom, DomRoot};
14use crate::dom::bindings::str::DOMString;
15use crate::dom::bindings::xmlname::matches_name_production;
16use crate::dom::element::Element;
17use crate::dom::html::htmlelement::HTMLElement;
18use crate::dom::node::NodeTraits;
19use crate::script_runtime::CanGc;
20
21#[dom_struct]
22pub(crate) struct DOMStringMap {
23 reflector_: Reflector,
24 element: Dom<HTMLElement>,
25}
26
27static DATA_PREFIX: &str = "data-";
28static DATA_HYPHEN_SEPARATOR: char = '\x2d';
29
30/// <https://html.spec.whatwg.org/multipage/#concept-domstringmap-pairs>
31fn to_camel_case(name: &str) -> Option<DOMString> {
32 // Step 2. For each content attribute on the DOMStringMap's associated element whose
33 // first five characters are the string "data-" and whose remaining characters (if any)
34 // do not include any ASCII upper alphas, in the order that those attributes
35 // are listed in the element's attribute list,
36 // add a name-value pair to list whose name is the attribute's name with the first
37 // five characters removed and whose value is the attribute's value.
38 let name = name.strip_prefix(DATA_PREFIX)?;
39 let has_uppercase = name.chars().any(|curr_char| curr_char.is_ascii_uppercase());
40 if has_uppercase {
41 return None;
42 }
43 // Step 3. For each name in list, for each U+002D HYPHEN-MINUS character (-)
44 // in the name that is followed by an ASCII lower alpha, remove the
45 // U+002D HYPHEN-MINUS character (-) and replace the character that followed
46 // it by the same character converted to ASCII uppercase.
47 let mut result = String::with_capacity(name.len().saturating_sub(DATA_PREFIX.len()));
48 let mut name_chars = name.chars().peekable();
49 while let Some(curr_char) = name_chars.next() {
50 // Note that we first need to peek, since we shouldn't advance the iterator twice
51 // in case there are two consecutive dashes and then followed by a ASCII lower alpha
52 if curr_char == DATA_HYPHEN_SEPARATOR &&
53 name_chars
54 .peek()
55 .is_some_and(|next_char| next_char.is_ascii_lowercase())
56 {
57 result.push(
58 name_chars
59 .next()
60 .expect("Already called peek")
61 .to_ascii_uppercase(),
62 );
63 continue;
64 }
65 result.push(curr_char);
66 }
67 // Step 1. Let list be an empty list of name-value pairs.
68 // Step 4. Return list.
69 //
70 // We do the iteration in the calling function, to avoid needlessly computing attribute
71 // values when we only need the names. Therefore, we only return the name.
72 Some(DOMString::from(result))
73}
74
75/// <https://html.spec.whatwg.org/multipage/#dom-domstringmap-setitem>
76/// and <https://html.spec.whatwg.org/multipage/#dom-domstringmap-removeitem>
77fn to_snake_case(name: &DOMString, should_throw: bool) -> Option<String> {
78 let name = name.str();
79 let mut result = String::with_capacity(DATA_PREFIX.len() + name.len());
80 // > Insert the string data- at the front of name.
81 result.push_str(DATA_PREFIX);
82 let mut name_chars = name.chars();
83 while let Some(curr_char) = name_chars.next() {
84 if curr_char == DATA_HYPHEN_SEPARATOR {
85 result.push(curr_char);
86
87 if let Some(next_char) = name_chars.next() {
88 // Only relevant for https://html.spec.whatwg.org/multipage/#dom-domstringmap-setitem
89 //
90 // > If name contains a U+002D HYPHEN-MINUS character (-) followed by an ASCII lower alpha,
91 // > then throw a "SyntaxError" DOMException.
92 if next_char.is_ascii_lowercase() {
93 if should_throw {
94 return None;
95 }
96 result.push(next_char);
97 } else {
98 // > For each ASCII upper alpha in name, insert a U+002D HYPHEN-MINUS character (-) before the character
99 // > and replace the character with the same character converted to ASCII lowercase.
100 result.push(DATA_HYPHEN_SEPARATOR);
101 result.push(next_char.to_ascii_lowercase());
102 }
103 }
104 } else {
105 // > For each ASCII upper alpha in name, insert a U+002D HYPHEN-MINUS character (-) before the character
106 // > and replace the character with the same character converted to ASCII lowercase.
107 if curr_char.is_ascii_uppercase() {
108 result.push(DATA_HYPHEN_SEPARATOR);
109 result.push(curr_char.to_ascii_lowercase());
110 } else {
111 result.push(curr_char);
112 }
113 }
114 }
115 Some(result)
116}
117
118impl DOMStringMap {
119 fn new_inherited(element: &HTMLElement) -> DOMStringMap {
120 DOMStringMap {
121 reflector_: Reflector::new(),
122 element: Dom::from_ref(element),
123 }
124 }
125
126 pub(crate) fn new(element: &HTMLElement, can_gc: CanGc) -> DomRoot<DOMStringMap> {
127 reflect_dom_object(
128 Box::new(DOMStringMap::new_inherited(element)),
129 &*element.owner_window(),
130 can_gc,
131 )
132 }
133
134 fn as_element(&self) -> &Element {
135 self.element.upcast::<Element>()
136 }
137}
138
139// https://html.spec.whatwg.org/multipage/#domstringmap
140impl DOMStringMapMethods<crate::DomTypeHolder> for DOMStringMap {
141 /// <https://html.spec.whatwg.org/multipage/#dom-domstringmap-removeitem>
142 fn NamedDeleter(&self, cx: &mut js::context::JSContext, name: DOMString) {
143 // Step 1. For each ASCII upper alpha in name, insert a U+002D HYPHEN-MINUS character (-) before the character
144 // and replace the character with the same character converted to ASCII lowercase.
145 // Step 2. Insert the string data- at the front of name.
146 let name = to_snake_case(&name, false).expect("Must always succeed");
147 // Step 3. Remove an attribute by name given name and the DOMStringMap's associated element.
148 self.as_element()
149 .remove_attribute(cx, &ns!(), &LocalName::from(name));
150 }
151
152 /// <https://html.spec.whatwg.org/multipage/#dom-domstringmap-setitem>
153 fn NamedSetter(
154 &self,
155 cx: &mut js::context::JSContext,
156 name: DOMString,
157 value: DOMString,
158 ) -> ErrorResult {
159 // Step 2. For each ASCII upper alpha in name, insert a U+002D HYPHEN-MINUS character (-)
160 // before the character and replace the character with the same character converted to ASCII lowercase.
161 // Step 3. Insert the string data- at the front of name.
162 let Some(name) = to_snake_case(&name, true) else {
163 // Step 1. If name contains a U+002D HYPHEN-MINUS character (-) followed by an ASCII lower alpha,
164 // then throw a "SyntaxError" DOMException.
165 return Err(Error::Syntax(None));
166 };
167 // Step 4. If name is not a valid attribute local name, then throw an "InvalidCharacterError" DOMException.
168 if !matches_name_production(&name) {
169 return Err(Error::InvalidCharacter(None));
170 }
171 // Step 5. Set an attribute value for the DOMStringMap's associated element using name and value.
172 let name = LocalName::from(name);
173 let element = self.as_element();
174 let value = element.parse_attribute(&ns!(), &name, value);
175 element.set_attribute_with_namespace(cx, name.clone(), value, name, ns!(), None);
176 Ok(())
177 }
178
179 /// <https://html.spec.whatwg.org/multipage/#dom-domstringmap-nameditem>
180 fn NamedGetter(&self, name: DOMString) -> Option<DOMString> {
181 // > To determine the value of a named property name for a DOMStringMap,
182 // > return the value component of the name-value pair whose name component is
183 // > name in the list returned from getting the DOMStringMap's name-value pairs.
184 self.as_element()
185 .attrs()
186 .borrow()
187 .iter()
188 .find(|attr| to_camel_case(attr.local_name()).as_ref() == Some(&name))
189 .map(|attr| DOMString::from(&**attr.value()))
190 }
191
192 /// <https://html.spec.whatwg.org/multipage/#the-domstringmap-interface:supported-property-names>
193 fn SupportedPropertyNames(&self, _: &NoGC) -> Vec<DOMString> {
194 // > The supported property names on a DOMStringMap object at any instant are
195 // > the names of each pair returned from getting the DOMStringMap's name-value
196 // > pairs at that instant, in the order returned.
197 self.as_element()
198 .attrs()
199 .borrow()
200 .iter()
201 .filter_map(|attr| to_camel_case(attr.local_name()))
202 .collect()
203 }
204}