use dom_struct::dom_struct;
use html5ever::{LocalName, ns};
use js::context::{JSContext, NoGC};
use script_bindings::reflector::{Reflector, reflect_dom_object};
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::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;
#[dom_struct]
pub(crate) struct DOMStringMap {
reflector_: Reflector,
element: Dom<HTMLElement>,
}
static DATA_PREFIX: &str = "data-";
static DATA_HYPHEN_SEPARATOR: char = '\x2d';
fn to_camel_case(name: &str) -> Option<DOMString> {
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;
}
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() {
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);
}
Some(DOMString::from(result))
}
fn to_snake_case(name: &DOMString, should_throw: bool) -> Option<String> {
let name = name.str();
let mut result = String::with_capacity(DATA_PREFIX.len() + name.len());
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() {
if next_char.is_ascii_lowercase() {
if should_throw {
return None;
}
result.push(next_char);
} else {
result.push(DATA_HYPHEN_SEPARATOR);
result.push(next_char.to_ascii_lowercase());
}
}
} else {
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(cx: &mut JSContext, element: &HTMLElement) -> DomRoot<DOMStringMap> {
reflect_dom_object(
cx,
Box::new(DOMStringMap::new_inherited(element)),
&*element.owner_window(),
)
}
fn as_element(&self) -> &Element {
self.element.upcast::<Element>()
}
}
impl DOMStringMapMethods<crate::DomTypeHolder> for DOMStringMap {
fn NamedDeleter(&self, cx: &mut JSContext, name: DOMString) {
let name = to_snake_case(&name, false).expect("Must always succeed");
self.as_element()
.remove_attribute(cx, &ns!(), &LocalName::from(name));
}
fn NamedSetter(&self, cx: &mut JSContext, name: DOMString, value: DOMString) -> ErrorResult {
let Some(name) = to_snake_case(&name, true) else {
return Err(Error::Syntax(None));
};
if !matches_name_production(&name) {
return Err(Error::InvalidCharacter(None));
}
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<DOMString> {
self.as_element()
.attrs()
.borrow()
.iter()
.find(|attr| to_camel_case(attr.local_name()).as_ref() == Some(&name))
.map(|attr| DOMString::from(&**attr.value()))
}
fn SupportedPropertyNames(&self, _: &NoGC) -> Vec<DOMString> {
self.as_element()
.attrs()
.borrow()
.iter()
.filter_map(|attr| to_camel_case(attr.local_name()))
.collect()
}
}