/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use dom_struct::dom_struct; use html5ever::{LocalName, ns}; use crate::dom::bindings::codegen::Bindings::DOMStringMapBinding::DOMStringMapMethods; use crate::dom::bindings::error::{Error, ErrorResult}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::str::DOMString; use crate::dom::bindings::xmlname::matches_name_production; use crate::dom::element::Element; use crate::dom::html::htmlelement::HTMLElement; use crate::dom::node::NodeTraits; use crate::script_runtime::CanGc; #[dom_struct] pub(crate) struct DOMStringMap { reflector_: Reflector, element: Dom, } static DATA_PREFIX: &str = "data-"; static DATA_HYPHEN_SEPARATOR: char = '\x2d'; /// fn to_camel_case(name: &str) -> Option { // Step 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, 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 is the attribute's value. let name = name.strip_prefix(DATA_PREFIX)?; let has_uppercase = name.chars().any(|curr_char| curr_char.is_ascii_uppercase()); if has_uppercase { return None; } // Step 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 that followed // it by the same character converted to ASCII uppercase. let mut result = String::with_capacity(name.len().saturating_sub(DATA_PREFIX.len())); let mut name_chars = name.chars().peekable(); while let Some(curr_char) = name_chars.next() { // Note that we first need to peek, since we shouldn't advance the iterator twice // in case there are two consecutive dashes and then followed by a ASCII lower alpha if curr_char == DATA_HYPHEN_SEPARATOR && name_chars .peek() .is_some_and(|next_char| next_char.is_ascii_lowercase()) { result.push( name_chars .next() .expect("Already called peek") .to_ascii_uppercase(), ); continue; } result.push(curr_char); } // Step 1. Let list be an empty list of name-value pairs. // Step 4. Return list. // // We do the iteration in the calling function, to avoid needlessly computing attribute // values when we only need the names. Therefore, we only return the name. Some(DOMString::from(result)) } /// /// and fn to_snake_case(name: &DOMString, should_throw: bool) -> Option { let name = name.str(); let mut result = String::with_capacity(DATA_PREFIX.len() + name.len()); // > Insert the string data- at the front of name. result.push_str(DATA_PREFIX); let mut name_chars = name.chars(); while let Some(curr_char) = name_chars.next() { if curr_char == DATA_HYPHEN_SEPARATOR { result.push(curr_char); if let Some(next_char) = name_chars.next() { // Only relevant for https://html.spec.whatwg.org/multipage/#dom-domstringmap-setitem // // > If name contains a U+002D HYPHEN-MINUS character (-) followed by an ASCII lower alpha, // > then throw a "SyntaxError" DOMException. if next_char.is_ascii_lowercase() { if should_throw { return None; } result.push(next_char); } else { // > 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. result.push(DATA_HYPHEN_SEPARATOR); result.push(next_char.to_ascii_lowercase()); } } } else { // > 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. if curr_char.is_ascii_uppercase() { result.push(DATA_HYPHEN_SEPARATOR); result.push(curr_char.to_ascii_lowercase()); } else { result.push(curr_char); } } } Some(result) } impl DOMStringMap { fn new_inherited(element: &HTMLElement) -> DOMStringMap { DOMStringMap { reflector_: Reflector::new(), element: Dom::from_ref(element), } } pub(crate) fn new(element: &HTMLElement, can_gc: CanGc) -> DomRoot { reflect_dom_object( Box::new(DOMStringMap::new_inherited(element)), &*element.owner_window(), can_gc, ) } fn as_element(&self) -> &Element { self.element.upcast::() } } // https://html.spec.whatwg.org/multipage/#domstringmap impl DOMStringMapMethods for DOMStringMap { /// fn NamedDeleter(&self, cx: &mut js::context::JSContext, name: DOMString) { // Step 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. // Step 2. Insert the string data- at the front of name. let name = to_snake_case(&name, false).expect("Must always succeed"); // Step 3. Remove an attribute by name given name and the DOMStringMap's associated element. self.as_element() .remove_attribute(&ns!(), &LocalName::from(name), CanGc::from_cx(cx)); } /// fn NamedSetter( &self, cx: &mut js::context::JSContext, name: DOMString, value: DOMString, ) -> ErrorResult { // Step 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. // Step 3. Insert the string data- at the front of name. let Some(name) = to_snake_case(&name, true) else { // Step 1. If name contains a U+002D HYPHEN-MINUS character (-) followed by an ASCII lower alpha, // then throw a "SyntaxError" DOMException. return Err(Error::Syntax(None)); }; // Step 4. If name is not a valid attribute local name, then throw an "InvalidCharacterError" DOMException. if !matches_name_production(&name) { return Err(Error::InvalidCharacter(None)); } // Step 5. Set an attribute value for the DOMStringMap's associated element using name and value. let name = LocalName::from(name); let element = self.as_element(); let value = element.parse_attribute(&ns!(), &name, value); element.set_attribute_with_namespace(cx, name.clone(), value, name, ns!(), None); Ok(()) } /// fn NamedGetter(&self, name: DOMString) -> Option { // > 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 DOMStringMap's name-value pairs. self.as_element() .attrs() .iter() .find(|attr| to_camel_case(attr.local_name()).as_ref() == Some(&name)) .map(|attr| DOMString::from(&**attr.value())) } /// fn SupportedPropertyNames(&self) -> Vec { // > 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. self.as_element() .attrs() .iter() .filter_map(|attr| to_camel_case(attr.local_name())) .collect() } }