use std::borrow::Cow;
use std::iter::FromIterator;
use std::rc::Rc;
use implicit_clone::ImplicitClone;
use indexmap::IndexSet;
use super::IntoPropValue;
use crate::utils::RcExt;
use crate::virtual_dom::AttrValue;
#[derive(Debug, Clone, Default)]
pub struct Classes {
set: Rc<IndexSet<AttrValue>>,
}
impl ImplicitClone for Classes {}
fn build_attr_value(first: AttrValue, rest: impl Iterator<Item = AttrValue> + Clone) -> AttrValue {
let mut s = String::with_capacity(
rest.clone()
.map(|class| class.len())
.chain([first.len(), rest.size_hint().0])
.sum(),
);
s.push_str(first.as_str());
for class in rest {
s.push(' ');
s.push_str(class.as_str());
}
s.into()
}
impl Classes {
#[inline]
pub fn new() -> Self {
Self {
set: Rc::new(IndexSet::new()),
}
}
#[inline]
pub fn with_capacity(n: usize) -> Self {
Self {
set: Rc::new(IndexSet::with_capacity(n)),
}
}
pub fn push<T: Into<Self>>(&mut self, class: T) {
let classes_to_add: Self = class.into();
if self.is_empty() {
*self = classes_to_add
} else {
Rc::make_mut(&mut self.set).extend(classes_to_add.set.iter().cloned())
}
}
pub unsafe fn unchecked_push<T: Into<AttrValue>>(&mut self, class: T) {
Rc::make_mut(&mut self.set).insert(class.into());
}
#[inline]
pub fn contains<T: AsRef<str>>(&self, class: T) -> bool {
self.set.contains(class.as_ref())
}
#[inline]
pub fn is_empty(&self) -> bool {
self.set.is_empty()
}
}
impl IntoPropValue<AttrValue> for Classes {
#[inline]
fn into_prop_value(self) -> AttrValue {
let mut classes = self.set.iter().cloned();
match classes.next() {
None => AttrValue::Static(""),
Some(class) if classes.len() == 0 => class,
Some(first) => build_attr_value(first, classes),
}
}
}
impl IntoPropValue<Option<AttrValue>> for Classes {
#[inline]
fn into_prop_value(self) -> Option<AttrValue> {
if self.is_empty() {
None
} else {
Some(self.into_prop_value())
}
}
}
impl IntoPropValue<Classes> for &'static str {
#[inline]
fn into_prop_value(self) -> Classes {
self.into()
}
}
impl<T: Into<Classes>> Extend<T> for Classes {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
iter.into_iter().for_each(|classes| self.push(classes))
}
}
impl<T: Into<Classes>> FromIterator<T> for Classes {
fn from_iter<IT: IntoIterator<Item = T>>(iter: IT) -> Self {
let mut classes = Self::new();
classes.extend(iter);
classes
}
}
impl IntoIterator for Classes {
type IntoIter = indexmap::set::IntoIter<AttrValue>;
type Item = AttrValue;
#[inline]
fn into_iter(self) -> Self::IntoIter {
RcExt::unwrap_or_clone(self.set).into_iter()
}
}
impl IntoIterator for &Classes {
type IntoIter = indexmap::set::IntoIter<AttrValue>;
type Item = AttrValue;
#[inline]
fn into_iter(self) -> Self::IntoIter {
(*self.set).clone().into_iter()
}
}
#[allow(clippy::to_string_trait_impl)]
impl ToString for Classes {
fn to_string(&self) -> String {
let mut iter = self.set.iter().cloned();
iter.next()
.map(|first| build_attr_value(first, iter))
.unwrap_or_default()
.to_string()
}
}
impl From<Cow<'static, str>> for Classes {
fn from(t: Cow<'static, str>) -> Self {
match t {
Cow::Borrowed(x) => Self::from(x),
Cow::Owned(x) => Self::from(x),
}
}
}
impl From<&'static str> for Classes {
fn from(t: &'static str) -> Self {
let set = t.split_whitespace().map(AttrValue::Static).collect();
Self { set: Rc::new(set) }
}
}
impl From<String> for Classes {
fn from(t: String) -> Self {
match t.contains(|c: char| c.is_whitespace()) {
false => match t.is_empty() {
true => Self::new(),
false => Self {
set: Rc::new(IndexSet::from_iter([AttrValue::from(t)])),
},
},
true => Self::from(&t),
}
}
}
impl From<&String> for Classes {
fn from(t: &String) -> Self {
let set = t
.split_whitespace()
.map(ToOwned::to_owned)
.map(AttrValue::from)
.collect();
Self { set: Rc::new(set) }
}
}
impl From<&AttrValue> for Classes {
fn from(t: &AttrValue) -> Self {
let set = t
.split_whitespace()
.map(ToOwned::to_owned)
.map(AttrValue::from)
.collect();
Self { set: Rc::new(set) }
}
}
impl From<AttrValue> for Classes {
fn from(t: AttrValue) -> Self {
match t.contains(|c: char| c.is_whitespace()) {
false => match t.is_empty() {
true => Self::new(),
false => Self {
set: Rc::new(IndexSet::from_iter([t])),
},
},
true => Self::from(&t),
}
}
}
impl<T: Into<Classes>> From<Option<T>> for Classes {
fn from(t: Option<T>) -> Self {
t.map(|x| x.into()).unwrap_or_default()
}
}
impl<T: Into<Classes> + Clone> From<&Option<T>> for Classes {
fn from(t: &Option<T>) -> Self {
Self::from(t.clone())
}
}
impl<T: Into<Classes>> From<Vec<T>> for Classes {
fn from(t: Vec<T>) -> Self {
Self::from_iter(t)
}
}
impl<T: Into<Classes> + Clone> From<&[T]> for Classes {
fn from(t: &[T]) -> Self {
t.iter().cloned().collect()
}
}
impl<T: Into<Classes>, const SIZE: usize> From<[T; SIZE]> for Classes {
fn from(t: [T; SIZE]) -> Self {
t.into_iter().collect()
}
}
impl PartialEq for Classes {
fn eq(&self, other: &Self) -> bool {
self.set.len() == other.set.len() && self.set.iter().eq(other.set.iter())
}
}
impl Eq for Classes {}
#[cfg(test)]
mod tests {
use super::*;
struct TestClass;
impl TestClass {
fn as_class(&self) -> &'static str {
"test-class"
}
}
impl From<TestClass> for Classes {
fn from(x: TestClass) -> Self {
Classes::from(x.as_class())
}
}
#[test]
fn it_is_initially_empty() {
let subject = Classes::new();
assert!(subject.is_empty());
}
#[test]
fn it_pushes_value() {
let mut subject = Classes::new();
subject.push("foo");
assert!(!subject.is_empty());
assert!(subject.contains("foo"));
}
#[test]
fn it_adds_values_via_extend() {
let mut other = Classes::new();
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
assert!(subject.contains("bar"));
}
#[test]
fn it_contains_both_values() {
let mut other = Classes::new();
other.push("bar");
let mut subject = Classes::new();
subject.extend(other);
subject.push("foo");
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn it_splits_class_with_spaces() {
let mut subject = Classes::new();
subject.push("foo bar");
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn push_and_contains_can_be_used_with_other_objects() {
let mut subject = Classes::new();
subject.push(TestClass);
let other_class: Option<TestClass> = None;
subject.push(other_class);
assert!(subject.contains(TestClass.as_class()));
}
#[test]
fn can_be_extended_with_another_class() {
let mut other = Classes::new();
other.push("foo");
other.push("bar");
let mut subject = Classes::new();
subject.extend(&other);
subject.extend(other);
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn can_be_collected() {
let classes = vec!["foo", "bar"];
let subject = classes.into_iter().collect::<Classes>();
assert!(subject.contains("foo"));
assert!(subject.contains("bar"));
}
#[test]
fn ignores_empty_string() {
let classes = String::from("");
let subject = Classes::from(classes);
assert!(subject.is_empty())
}
}