1use std::borrow::Cow;
2use std::rc::Rc;
3
4use indexmap::IndexSet;
5
6use super::IntoPropValue;
7use crate::html::ImplicitClone;
8use crate::utils::RcExt;
9use crate::virtual_dom::AttrValue;
10
11#[derive(Debug, Clone, ImplicitClone, Default)]
15pub struct Classes {
16 set: Rc<IndexSet<AttrValue>>,
17}
18
19fn build_attr_value(first: AttrValue, rest: impl Iterator<Item = AttrValue> + Clone) -> AttrValue {
24 let mut s = String::with_capacity(
27 rest.clone()
28 .map(|class| class.len())
29 .chain([first.len(), rest.size_hint().0])
30 .sum(),
31 );
32
33 s.push_str(first.as_str());
34 for class in rest {
36 s.push(' ');
37 s.push_str(class.as_str());
38 }
39 s.into()
40}
41
42impl Classes {
43 #[inline]
45 pub fn new() -> Self {
46 Self {
47 set: Rc::new(IndexSet::new()),
48 }
49 }
50
51 #[inline]
54 pub fn with_capacity(n: usize) -> Self {
55 Self {
56 set: Rc::new(IndexSet::with_capacity(n)),
57 }
58 }
59
60 pub fn push<T: Into<Self>>(&mut self, class: T) {
64 let classes_to_add: Self = class.into();
65 if self.is_empty() {
66 *self = classes_to_add
67 } else {
68 Rc::make_mut(&mut self.set).extend(classes_to_add.set.iter().cloned())
69 }
70 }
71
72 pub unsafe fn unchecked_push<T: Into<AttrValue>>(&mut self, class: T) {
84 Rc::make_mut(&mut self.set).insert(class.into());
85 }
86
87 #[inline]
89 pub fn contains<T: AsRef<str>>(&self, class: T) -> bool {
90 self.set.contains(class.as_ref())
91 }
92
93 #[inline]
95 pub fn is_empty(&self) -> bool {
96 self.set.is_empty()
97 }
98}
99
100impl IntoPropValue<AttrValue> for Classes {
101 #[inline]
102 fn into_prop_value(self) -> AttrValue {
103 let mut classes = self.set.iter().cloned();
104
105 match classes.next() {
106 None => AttrValue::Static(""),
107 Some(class) if classes.len() == 0 => class,
108 Some(first) => build_attr_value(first, classes),
109 }
110 }
111}
112
113impl IntoPropValue<Option<AttrValue>> for Classes {
114 #[inline]
115 fn into_prop_value(self) -> Option<AttrValue> {
116 if self.is_empty() {
117 None
118 } else {
119 Some(self.into_prop_value())
120 }
121 }
122}
123
124impl IntoPropValue<Classes> for &'static str {
125 #[inline]
126 fn into_prop_value(self) -> Classes {
127 self.into()
128 }
129}
130
131impl<T: Into<Classes>> Extend<T> for Classes {
132 fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
133 iter.into_iter().for_each(|classes| self.push(classes))
134 }
135}
136
137impl<T: Into<Classes>> FromIterator<T> for Classes {
138 fn from_iter<IT: IntoIterator<Item = T>>(iter: IT) -> Self {
139 let mut classes = Self::new();
140 classes.extend(iter);
141 classes
142 }
143}
144
145impl IntoIterator for Classes {
146 type IntoIter = indexmap::set::IntoIter<AttrValue>;
147 type Item = AttrValue;
148
149 #[inline]
150 fn into_iter(self) -> Self::IntoIter {
151 RcExt::unwrap_or_clone(self.set).into_iter()
152 }
153}
154
155impl IntoIterator for &Classes {
156 type IntoIter = indexmap::set::IntoIter<AttrValue>;
157 type Item = AttrValue;
158
159 #[inline]
160 fn into_iter(self) -> Self::IntoIter {
161 (*self.set).clone().into_iter()
162 }
163}
164
165#[expect(clippy::to_string_trait_impl)]
166impl ToString for Classes {
167 fn to_string(&self) -> String {
168 let mut iter = self.set.iter().cloned();
169
170 iter.next()
171 .map(|first| build_attr_value(first, iter))
172 .unwrap_or_default()
173 .to_string()
174 }
175}
176
177impl From<Cow<'static, str>> for Classes {
178 fn from(t: Cow<'static, str>) -> Self {
179 match t {
180 Cow::Borrowed(x) => Self::from(x),
181 Cow::Owned(x) => Self::from(x),
182 }
183 }
184}
185
186impl From<&'static str> for Classes {
187 fn from(t: &'static str) -> Self {
188 let set = t.split_whitespace().map(AttrValue::Static).collect();
189 Self { set: Rc::new(set) }
190 }
191}
192
193impl From<String> for Classes {
194 fn from(t: String) -> Self {
195 match t.contains(|c: char| c.is_whitespace()) {
196 false => match t.is_empty() {
200 true => Self::new(),
201 false => Self {
202 set: Rc::new(IndexSet::from_iter([AttrValue::from(t)])),
203 },
204 },
205 true => Self::from(&t),
206 }
207 }
208}
209
210impl From<&String> for Classes {
211 fn from(t: &String) -> Self {
212 let set = t
213 .split_whitespace()
214 .map(ToOwned::to_owned)
215 .map(AttrValue::from)
216 .collect();
217 Self { set: Rc::new(set) }
218 }
219}
220
221impl From<&AttrValue> for Classes {
222 fn from(t: &AttrValue) -> Self {
223 let set = t
224 .split_whitespace()
225 .map(ToOwned::to_owned)
226 .map(AttrValue::from)
227 .collect();
228 Self { set: Rc::new(set) }
229 }
230}
231
232impl From<AttrValue> for Classes {
233 fn from(t: AttrValue) -> Self {
234 match t.contains(|c: char| c.is_whitespace()) {
235 false => match t.is_empty() {
239 true => Self::new(),
240 false => Self {
241 set: Rc::new(IndexSet::from_iter([t])),
242 },
243 },
244 true => Self::from(&t),
245 }
246 }
247}
248
249impl<T: Into<Classes>> From<Option<T>> for Classes {
250 fn from(t: Option<T>) -> Self {
251 t.map(|x| x.into()).unwrap_or_default()
252 }
253}
254
255impl<T: Into<Classes> + Clone> From<&Option<T>> for Classes {
256 fn from(t: &Option<T>) -> Self {
257 Self::from(t.clone())
258 }
259}
260
261impl<T: Into<Classes>> From<Vec<T>> for Classes {
262 fn from(t: Vec<T>) -> Self {
263 Self::from_iter(t)
264 }
265}
266
267impl<T: Into<Classes> + Clone> From<&[T]> for Classes {
268 fn from(t: &[T]) -> Self {
269 t.iter().cloned().collect()
270 }
271}
272
273impl<T: Into<Classes>, const SIZE: usize> From<[T; SIZE]> for Classes {
274 fn from(t: [T; SIZE]) -> Self {
275 t.into_iter().collect()
276 }
277}
278
279impl From<&Classes> for Classes {
280 fn from(c: &Classes) -> Self {
281 c.clone()
282 }
283}
284
285impl From<&Classes> for AttrValue {
286 fn from(c: &Classes) -> Self {
287 c.clone().into_prop_value()
288 }
289}
290
291impl PartialEq for Classes {
292 fn eq(&self, other: &Self) -> bool {
293 self.set.len() == other.set.len() && self.set.iter().eq(other.set.iter())
294 }
295}
296
297impl Eq for Classes {}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 struct TestClass;
304
305 impl TestClass {
306 fn as_class(&self) -> &'static str {
307 "test-class"
308 }
309 }
310
311 impl From<TestClass> for Classes {
312 fn from(x: TestClass) -> Self {
313 Classes::from(x.as_class())
314 }
315 }
316
317 #[test]
318 fn it_is_initially_empty() {
319 let subject = Classes::new();
320 assert!(subject.is_empty());
321 }
322
323 #[test]
324 fn it_pushes_value() {
325 let mut subject = Classes::new();
326 subject.push("foo");
327 assert!(!subject.is_empty());
328 assert!(subject.contains("foo"));
329 }
330
331 #[test]
332 fn it_adds_values_via_extend() {
333 let mut other = Classes::new();
334 other.push("bar");
335 let mut subject = Classes::new();
336 subject.extend(other);
337 assert!(subject.contains("bar"));
338 }
339
340 #[test]
341 fn it_contains_both_values() {
342 let mut other = Classes::new();
343 other.push("bar");
344 let mut subject = Classes::new();
345 subject.extend(other);
346 subject.push("foo");
347 assert!(subject.contains("foo"));
348 assert!(subject.contains("bar"));
349 }
350
351 #[test]
352 fn it_splits_class_with_spaces() {
353 let mut subject = Classes::new();
354 subject.push("foo bar");
355 assert!(subject.contains("foo"));
356 assert!(subject.contains("bar"));
357 }
358
359 #[test]
360 fn push_and_contains_can_be_used_with_other_objects() {
361 let mut subject = Classes::new();
362 subject.push(TestClass);
363 let other_class: Option<TestClass> = None;
364 subject.push(other_class);
365 assert!(subject.contains(TestClass.as_class()));
366 }
367
368 #[test]
369 fn can_be_extended_with_another_class() {
370 let mut other = Classes::new();
371 other.push("foo");
372 other.push("bar");
373 let mut subject = Classes::new();
374 subject.extend(&other);
375 subject.extend(other);
376 assert!(subject.contains("foo"));
377 assert!(subject.contains("bar"));
378 }
379
380 #[test]
381 fn can_be_collected() {
382 let classes = vec!["foo", "bar"];
383 let subject = classes.into_iter().collect::<Classes>();
384 assert!(subject.contains("foo"));
385 assert!(subject.contains("bar"));
386 }
387
388 #[test]
389 fn ignores_empty_string() {
390 let classes = String::from("");
391 let subject = Classes::from(classes);
392 assert!(subject.is_empty())
393 }
394}