use crate::{builder_fn, util, AutoDefault, CowStr};
use std::collections::HashSet;
#[derive(AutoDefault, Clone, Debug, PartialEq)]
pub enum ClassesOp {
#[default]
Add,
Prepend,
Remove,
Replace(CowStr),
Toggle,
Set,
}
#[derive(AutoDefault, Clone, Debug)]
pub struct Classes(Vec<String>);
impl Classes {
pub fn new(classes: impl AsRef<str>) -> Self {
Self::default().with_classes(ClassesOp::default(), classes)
}
#[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
let Some(normalized) =
util::normalize_ascii_or_empty(classes.as_ref(), "Classes::with_classes")
else {
return self;
};
match op {
ClassesOp::Add => {
self.add(normalized.as_ref().split_ascii_whitespace(), self.0.len());
}
ClassesOp::Prepend => {
self.add(normalized.as_ref().split_ascii_whitespace(), 0);
}
ClassesOp::Remove => {
let mut classes_to_remove = normalized.as_ref().split_ascii_whitespace();
let Some(first) = classes_to_remove.next() else {
return self;
};
let Some(second) = classes_to_remove.next() else {
self.0.retain(|c| c != first);
return self;
};
let mut to_remove: HashSet<&str> = HashSet::new();
to_remove.insert(first);
to_remove.insert(second);
for class in classes_to_remove {
to_remove.insert(class);
}
self.0.retain(|c| !to_remove.contains(c.as_str()));
}
ClassesOp::Replace(classes_to_replace) => {
let Some(classes_to_replace) = util::normalize_ascii_or_empty(
classes_to_replace.as_ref(),
"ClassesOp::Replace",
) else {
return self;
};
let mut pos = self.0.len();
let mut replaced = false;
for class in classes_to_replace.as_ref().split_ascii_whitespace() {
if let Some(replace_pos) = self.0.iter().position(|c| c == class) {
self.0.remove(replace_pos);
pos = pos.min(replace_pos);
replaced = true;
}
}
if replaced {
self.add(normalized.as_ref().split_ascii_whitespace(), pos);
}
}
ClassesOp::Toggle => {
for class in normalized.as_ref().split_ascii_whitespace() {
if let Some(pos) = self.0.iter().position(|c| c == class) {
self.0.remove(pos);
} else {
self.0.push(class.to_string());
}
}
}
ClassesOp::Set => {
self.0.clear();
self.add(normalized.as_ref().split_ascii_whitespace(), 0);
}
}
self
}
#[inline]
fn add<'a, I>(&mut self, classes: I, mut pos: usize)
where
I: IntoIterator<Item = &'a str>,
{
for class in classes {
if !self.0.iter().any(|c| c == class) {
let class = class.to_string();
if pos >= self.0.len() {
self.0.push(class);
} else {
self.0.insert(pos, class);
}
pos += 1;
}
}
}
pub fn get(&self) -> Option<String> {
if self.0.is_empty() {
None
} else {
Some(self.0.join(" "))
}
}
pub fn contains(&self, classes: impl AsRef<str>) -> bool {
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
return false;
};
normalized
.as_ref()
.split_ascii_whitespace()
.all(|class| self.0.iter().any(|c| c == class))
}
pub fn contains_any(&self, classes: impl AsRef<str>) -> bool {
let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else {
return false;
};
normalized
.as_ref()
.split_ascii_whitespace()
.any(|class| self.0.iter().any(|c| c == class))
}
}