use core::fmt;
use std::{
borrow::Cow,
collections::{hash_set, HashSet},
error::Error as StdError,
iter::FusedIterator,
};
use super::{sealed, write_escaped, Attribute};
use crate::Result;
#[derive(Debug, Clone)]
pub enum ClassError {
ClassWithWhitespace,
EmptyClass,
}
impl fmt::Display for ClassError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ClassWithWhitespace => write!(f, "HTML class with ASCII whitespace"),
Self::EmptyClass => write!(f, "Empty HTML class"),
}
}
}
impl StdError for ClassError {}
#[derive(Debug, Clone, Default)]
pub struct ClassAttr {
classes: HashSet<Cow<'static, str>>,
}
impl ClassAttr {
pub fn empty() -> Self {
ClassAttr {
classes: HashSet::new(),
}
}
pub fn new(classes: &'static str) -> Result<Self, ClassError> {
let mut slf = Self::empty();
slf.add_multiple(classes)?;
Ok(slf)
}
#[inline]
pub fn is_empty(&self) -> bool {
self.classes.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.classes.len()
}
pub fn value(&self) -> impl fmt::Display + Copy + '_ {
#[derive(Clone, Copy)]
struct Display<'a>(&'a ClassAttr);
impl fmt::Display for Display<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut space = false;
for class in &self.0.classes {
if space {
write!(f, " ")?;
}
fmt::Display::fmt(&class, f)?;
space = true;
}
Ok(())
}
}
Display(self)
}
pub fn add(&mut self, class: impl Into<Cow<'static, str>>) -> Result<bool, ClassError> {
let class = class.into();
if class.is_empty() {
return Err(ClassError::EmptyClass);
} else if class.contains(|ch: char| ch.is_ascii_whitespace()) {
return Err(ClassError::ClassWithWhitespace);
}
Ok(self.classes.insert(class))
}
pub fn add_multiple(&mut self, classes: &'static str) -> Result<(), ClassError> {
for class in classes.split_ascii_whitespace() {
self.classes.insert(class.into());
}
Ok(())
}
pub fn add_iter(
&mut self,
classes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
) -> Result<(), ClassError> {
for class in classes {
self.add(class)?;
}
Ok(())
}
pub fn remove(&mut self, class: &str) -> bool {
self.classes.remove(class)
}
pub fn remove_multiple(&mut self, classes: &str) {
for class in classes.split_ascii_whitespace() {
self.remove(class);
}
}
pub fn remove_iter(&mut self, classes: impl IntoIterator<Item = impl AsRef<str>>) {
for class in classes {
self.remove(class.as_ref());
}
}
pub fn iter(&self) -> Iter<'_> {
Iter(self.classes.iter())
}
}
impl IntoIterator for ClassAttr {
type Item = Cow<'static, str>;
type IntoIter = hash_set::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.classes.into_iter()
}
}
impl<'a> IntoIterator for &'a ClassAttr {
type Item = &'a str;
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct Iter<'a>(hash_set::Iter<'a, Cow<'static, str>>);
impl<'a> Iterator for Iter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
Some(&**self.0.next()?)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl ExactSizeIterator for Iter<'_> {
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for Iter<'_> {}
impl sealed::AttributeSeal for ClassAttr {}
impl Attribute for ClassAttr {
fn render_into(&self, writer: &mut (impl std::fmt::Write + ?Sized)) -> Result<()> {
writer.write_str(" class=\"")?;
write_escaped(writer, &self.value())?;
writer.write_char('"')?;
Ok(())
}
}
#[test]
fn test_class_attr() -> Result<(), ClassError> {
use itertools::Itertools;
let mut attr = ClassAttr::new("hello world this-is w-[1px] m-4")?;
attr.add_iter(["josh", "states", "line", "united"])?;
attr.remove_multiple("world josh");
attr.remove_iter(["this-is", "line"]);
assert_eq!(
attr.iter().sorted().join(" "),
"hello m-4 states united w-[1px]"
);
Ok(())
}