use std::collections::HashMap;
use topcoat_core::context::Cx;
use crate::{
Attribute, AttributeValueViewParts, AttributeViewParts, HtmlContext, PartsWriter, ViewPart,
ViewParts,
};
#[derive(Debug, Default, Clone)]
pub struct Attributes {
map: HashMap<String, ViewPart>,
}
impl Attributes {
#[inline]
#[must_use]
pub fn new() -> Self {
Attributes::default()
}
#[inline]
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
map: HashMap::with_capacity(capacity),
}
}
#[inline]
pub fn contains_key(&self, k: impl AsRef<str>) -> bool {
self.map.contains_key(k.as_ref())
}
#[inline]
pub fn get(&self, k: impl AsRef<str>) -> Option<&ViewPart> {
self.map.get(k.as_ref())
}
#[inline]
pub fn insert(
&mut self,
cx: &Cx,
k: impl Into<String>,
v: impl AttributeValueViewParts,
) -> Option<ViewPart> {
if v.attribute_present() {
let mut view_parts = ViewParts::new();
v.into_view_parts(
cx,
&mut PartsWriter::new(&mut view_parts, HtmlContext::AttributeValue),
);
self.map.insert(k.into(), view_parts.into())
} else {
self.map.insert(k.into(), ViewPart::empty())
}
}
#[inline]
pub fn remove(&mut self, k: impl AsRef<str>) -> Option<ViewPart> {
self.map.remove(k.as_ref())
}
#[inline]
pub fn clear(&mut self) {
self.map.clear();
}
#[inline]
pub fn extend(&mut self, iter: impl IntoIterator<Item = (String, ViewPart)>) {
self.map.extend(iter);
}
#[inline]
#[must_use]
pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
self.into_iter()
}
}
impl AttributeViewParts for Attributes {
fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
for (key, value) in self {
Attribute::new(key, value).into_view_parts(cx, parts);
}
}
}
impl IntoIterator for Attributes {
type Item = (String, ViewPart);
type IntoIter = std::collections::hash_map::IntoIter<String, ViewPart>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.map.into_iter()
}
}
impl<'a> IntoIterator for &'a Attributes {
type Item = (&'a String, &'a ViewPart);
type IntoIter = std::collections::hash_map::Iter<'a, String, ViewPart>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.map.iter()
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use topcoat_core::context::Cx;
use super::*;
use crate::View;
fn render(attrs: Attributes) -> String {
let mut parts = ViewParts::new();
attrs.into_view_parts(
&Cx::default(),
&mut PartsWriter::new(&mut parts, HtmlContext::AttributeValue),
);
View::new(parts).render(&Cx::default())
}
#[test]
fn new_is_empty() {
let attrs = Attributes::new();
assert!(!attrs.contains_key("class"));
assert_eq!(attrs.iter().count(), 0);
}
#[test]
fn with_capacity_is_empty() {
let attrs = Attributes::with_capacity(4);
assert_eq!(attrs.iter().count(), 0);
}
#[test]
fn insert_then_contains_key() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
assert!(attrs.contains_key("class"));
assert!(!attrs.contains_key("id"));
}
#[test]
fn insert_returns_none_for_new_key() {
let mut attrs = Attributes::new();
assert!(attrs.insert(&Cx::default(), "class", "button").is_none());
}
#[test]
fn insert_replaces_existing_value() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
let previous = attrs.insert(&Cx::default(), "class", "link");
assert!(previous.is_some());
assert_eq!(render(attrs), " class=\"link\"");
}
#[test]
fn get_returns_inserted_value() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
assert!(attrs.get("class").is_some());
assert!(attrs.get("missing").is_none());
}
#[test]
fn remove_returns_value_and_deletes_entry() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
assert!(attrs.remove("class").is_some());
assert!(!attrs.contains_key("class"));
assert!(attrs.remove("class").is_none());
}
#[test]
fn clear_removes_all_entries() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
attrs.insert(&Cx::default(), "id", "submit");
attrs.clear();
assert_eq!(attrs.iter().count(), 0);
assert!(!attrs.contains_key("class"));
}
#[test]
fn renders_single_attribute() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
assert_eq!(render(attrs), " class=\"button\"");
}
#[test]
fn renders_multiple_attributes() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
attrs.insert(&Cx::default(), "id", "submit");
let rendered = render(attrs);
let parts: HashSet<&str> = rendered
.split_terminator(' ')
.filter(|s| !s.is_empty())
.collect();
let expected: HashSet<&str> = ["class=\"button\"", "id=\"submit\""].into_iter().collect();
assert_eq!(parts, expected);
}
#[test]
fn escapes_attribute_value() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "data-x", "a\"b<c");
assert_eq!(render(attrs), " data-x=\"a"b<c\"");
}
#[test]
fn omits_false_boolean_attribute() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "disabled", false);
assert_eq!(render(attrs), "");
}
#[test]
fn renders_true_boolean_attribute() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "disabled", true);
assert_eq!(render(attrs), " disabled=\"true\"");
}
#[test]
fn omits_none_option_attribute() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "title", Option::<&str>::None);
assert!(attrs.get("title").unwrap().is_empty());
assert_eq!(render(attrs), "");
}
#[test]
fn renders_some_option_attribute() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "title", Some("hello"));
assert_eq!(render(attrs), " title=\"hello\"");
}
#[test]
fn iter_yields_inserted_entries() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
attrs.insert(&Cx::default(), "id", "submit");
let keys: HashSet<&str> = attrs.iter().map(|(k, _)| k.as_str()).collect();
let expected: HashSet<&str> = ["class", "id"].into_iter().collect();
assert_eq!(keys, expected);
}
#[test]
fn into_iter_yields_inserted_entries() {
let mut attrs = Attributes::new();
attrs.insert(&Cx::default(), "class", "button");
attrs.insert(&Cx::default(), "id", "submit");
let keys: HashSet<String> = attrs.into_iter().map(|(k, _)| k).collect();
let expected: HashSet<String> = ["class", "id"].into_iter().map(String::from).collect();
assert_eq!(keys, expected);
}
}