Skip to main content

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::{JSContext, NoGC};
8use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
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;
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(cx: &mut JSContext, element: &HTMLElement) -> DomRoot<DOMStringMap> {
126        reflect_dom_object_with_cx(
127            Box::new(DOMStringMap::new_inherited(element)),
128            &*element.owner_window(),
129            cx,
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 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(cx, &ns!(), &LocalName::from(name));
149    }
150
151    /// <https://html.spec.whatwg.org/multipage/#dom-domstringmap-setitem>
152    fn NamedSetter(&self, cx: &mut JSContext, name: DOMString, value: DOMString) -> ErrorResult {
153        // Step 2. For each ASCII upper alpha in name, insert a U+002D HYPHEN-MINUS character (-)
154        // before the character and replace the character with the same character converted to ASCII lowercase.
155        // Step 3. Insert the string data- at the front of name.
156        let Some(name) = to_snake_case(&name, true) else {
157            // Step 1. If name contains a U+002D HYPHEN-MINUS character (-) followed by an ASCII lower alpha,
158            // then throw a "SyntaxError" DOMException.
159            return Err(Error::Syntax(None));
160        };
161        // Step 4. If name is not a valid attribute local name, then throw an "InvalidCharacterError" DOMException.
162        if !matches_name_production(&name) {
163            return Err(Error::InvalidCharacter(None));
164        }
165        // Step 5. Set an attribute value for the DOMStringMap's associated element using name and value.
166        let name = LocalName::from(name);
167        let element = self.as_element();
168        let value = element.parse_attribute(&ns!(), &name, value);
169        element.set_attribute_with_namespace(cx, name.clone(), value, name, ns!(), None);
170        Ok(())
171    }
172
173    /// <https://html.spec.whatwg.org/multipage/#dom-domstringmap-nameditem>
174    fn NamedGetter(&self, name: DOMString) -> Option<DOMString> {
175        // > To determine the value of a named property name for a DOMStringMap,
176        // > return the value component of the name-value pair whose name component is
177        // > name in the list returned from getting the DOMStringMap's name-value pairs.
178        self.as_element()
179            .attrs()
180            .borrow()
181            .iter()
182            .find(|attr| to_camel_case(attr.local_name()).as_ref() == Some(&name))
183            .map(|attr| DOMString::from(&**attr.value()))
184    }
185
186    /// <https://html.spec.whatwg.org/multipage/#the-domstringmap-interface:supported-property-names>
187    fn SupportedPropertyNames(&self, _: &NoGC) -> Vec<DOMString> {
188        // > The supported property names on a DOMStringMap object at any instant are
189        // > the names of each pair returned from getting the DOMStringMap's name-value
190        // > pairs at that instant, in the order returned.
191        self.as_element()
192            .attrs()
193            .borrow()
194            .iter()
195            .filter_map(|attr| to_camel_case(attr.local_name()))
196            .collect()
197    }
198}