use crate::accessor::{NodeMut, NodeRef};
use crate::error::Result;
pub struct DomStringMap<'a, Ext: 'static> {
node: NodeRef<'a, Ext>,
}
impl<'a, Ext: 'static> DomStringMap<'a, Ext> {
pub fn new(node: NodeRef<'a, Ext>) -> Self {
Self { node }
}
pub fn get(&self, key: &str) -> Option<&'a str> {
let attr = camel_to_data_attr(key)?;
self.node.get_attribute(&attr)
}
pub fn contains_key(&self, key: &str) -> bool {
self.get(key).is_some()
}
pub fn iter(&self) -> impl Iterator<Item = (String, &'a str)> + 'a {
self.node
.dom
.attributes(self.node.id)
.filter_map(|(name, value)| Some((data_attr_to_camel(name)?, value)))
}
pub fn len(&self) -> usize {
self.iter().count()
}
pub fn is_empty(&self) -> bool {
self.iter().next().is_none()
}
}
pub struct DomStringMapMut<'a, Ext: 'static> {
node: NodeMut<'a, Ext>,
}
impl<'a, Ext: 'static> DomStringMapMut<'a, Ext> {
pub fn new(node: NodeMut<'a, Ext>) -> Self {
Self { node }
}
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
let Some(attr) = camel_to_data_attr(key) else {
return Ok(()); };
self.node.set_attribute(&attr, value).map(|_| ())
}
pub fn remove(&mut self, key: &str) -> Result<bool> {
let Some(attr) = camel_to_data_attr(key) else {
return Ok(false);
};
self.node.remove_attribute(&attr)
}
}
fn camel_to_data_attr(key: &str) -> Option<String> {
if key.is_empty() {
return None;
}
let mut out = String::with_capacity(key.len() + 6);
out.push_str("data-");
for c in key.chars() {
if c.is_ascii_uppercase() {
out.push('-');
out.push(c.to_ascii_lowercase());
} else {
out.push(c);
}
}
Some(out)
}
fn data_attr_to_camel(name: &str) -> Option<String> {
let suffix = name.strip_prefix("data-")?;
if suffix.is_empty() {
return None;
}
if suffix.chars().any(|c| c.is_ascii_uppercase()) {
return None;
}
let mut out = String::with_capacity(suffix.len());
let mut chars = suffix.chars();
while let Some(c) = chars.next() {
if c == '-' {
match chars.next() {
Some(next) if next.is_ascii_lowercase() => {
out.push(next.to_ascii_uppercase());
}
_ => return None,
}
} else {
out.push(c);
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Dom;
#[test]
fn camel_to_data_attr_simple_key() {
assert_eq!(camel_to_data_attr("foo"), Some("data-foo".into()));
}
#[test]
fn camel_to_data_attr_uppercase_becomes_hyphen_lowercase() {
assert_eq!(camel_to_data_attr("fooBar"), Some("data-foo-bar".into()));
}
#[test]
fn camel_to_data_attr_consecutive_uppercase_each_gets_hyphen() {
assert_eq!(
camel_to_data_attr("fooBARbaz"),
Some("data-foo-b-a-rbaz".into())
);
}
#[test]
fn camel_to_data_attr_empty_key_rejected() {
assert_eq!(camel_to_data_attr(""), None);
}
#[test]
fn data_attr_to_camel_simple() {
assert_eq!(data_attr_to_camel("data-foo"), Some("foo".into()));
}
#[test]
fn data_attr_to_camel_hyphen_lowercase_becomes_uppercase() {
assert_eq!(data_attr_to_camel("data-foo-bar"), Some("fooBar".into()));
}
#[test]
fn data_attr_to_camel_round_trips_consecutive_uppercase_pattern() {
assert_eq!(
data_attr_to_camel("data-foo-b-a-rbaz"),
Some("fooBARbaz".into())
);
}
#[test]
fn data_attr_to_camel_rejects_non_data_prefix() {
assert_eq!(data_attr_to_camel("foo"), None);
assert_eq!(data_attr_to_camel("class"), None);
}
#[test]
fn data_attr_to_camel_rejects_empty_suffix() {
assert_eq!(data_attr_to_camel("data-"), None);
}
#[test]
fn data_attr_to_camel_rejects_uppercase_in_attr() {
assert_eq!(data_attr_to_camel("data-Foo"), None);
}
#[test]
fn data_attr_to_camel_rejects_trailing_hyphen() {
assert_eq!(data_attr_to_camel("data-foo-"), None);
}
#[test]
fn data_attr_to_camel_rejects_double_hyphen() {
assert_eq!(data_attr_to_camel("data-foo--bar"), None);
}
fn element_with_attrs(pairs: &[(&str, &str)]) -> (Dom, crate::node_id::NodeId) {
let mut dom: Dom = Dom::new();
let el = dom.create_element("div");
for (k, v) in pairs {
dom.set_attribute(el, k, v).unwrap();
}
(dom, el)
}
#[test]
fn get_foo_bar_reads_data_foo_bar() {
let (dom, el) = element_with_attrs(&[("data-foo-bar", "value!")]);
let ds = DomStringMap::new(dom.node(el));
assert_eq!(ds.get("fooBar"), Some("value!"));
assert!(ds.contains_key("fooBar"));
}
#[test]
fn get_missing_returns_none() {
let (dom, el) = element_with_attrs(&[]);
let ds = DomStringMap::new(dom.node(el));
assert_eq!(ds.get("foo"), None);
assert!(!ds.contains_key("foo"));
}
#[test]
fn iter_returns_round_tripable_attrs_with_camel_keys() {
let (dom, el) = element_with_attrs(&[
("data-foo", "1"),
("data-foo-bar", "2"),
("class", "ignored"),
("id", "ignored"),
]);
let ds = DomStringMap::new(dom.node(el));
let mut entries: Vec<(String, &str)> = ds.iter().collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
assert_eq!(
entries,
vec![("foo".to_string(), "1"), ("fooBar".to_string(), "2"),]
);
}
#[test]
fn iter_skips_invalid_data_attrs() {
let (dom, el) = element_with_attrs(&[("data-foo", "ok"), ("data-", "skipped-empty")]);
let ds = DomStringMap::new(dom.node(el));
let entries: Vec<(String, &str)> = ds.iter().collect();
assert_eq!(entries, vec![("foo".to_string(), "ok")]);
}
#[test]
fn len_counts_only_valid_data_attrs() {
let (dom, el) = element_with_attrs(&[
("data-foo", "1"),
("data-bar-baz", "2"),
("class", "ignored"),
]);
let ds = DomStringMap::new(dom.node(el));
assert_eq!(ds.len(), 2);
assert!(!ds.is_empty());
}
#[test]
fn empty_dataset_is_empty() {
let (dom, el) = element_with_attrs(&[("class", "not-data")]);
let ds = DomStringMap::new(dom.node(el));
assert_eq!(ds.len(), 0);
assert!(ds.is_empty());
}
#[test]
fn set_writes_camel_key_as_kebab_data_attr() {
let (mut dom, el) = element_with_attrs(&[]);
let mut ds = DomStringMapMut::new(dom.node_mut(el));
ds.set("fooBar", "X").unwrap();
assert_eq!(dom.node(el).get_attribute("data-foo-bar"), Some("X"));
}
#[test]
fn set_then_get_round_trips() {
let (mut dom, el) = element_with_attrs(&[]);
{
let mut ds = DomStringMapMut::new(dom.node_mut(el));
ds.set("fooBARbaz", "weird").unwrap();
}
let ds_read = DomStringMap::new(dom.node(el));
assert_eq!(ds_read.get("fooBARbaz"), Some("weird"));
}
#[test]
fn remove_returns_true_when_present() {
let (mut dom, el) = element_with_attrs(&[("data-foo", "1")]);
let mut ds = DomStringMapMut::new(dom.node_mut(el));
assert!(ds.remove("foo").unwrap());
assert!(!ds.remove("foo").unwrap(), "second remove finds nothing");
}
#[test]
fn set_empty_key_is_ignored() {
let (mut dom, el) = element_with_attrs(&[]);
let mut ds = DomStringMapMut::new(dom.node_mut(el));
ds.set("", "value").unwrap();
assert!(dom.node(el).get_attribute("data-").is_none());
}
}