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