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