1use crate::responsive::Breakpoint;
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone, PartialEq)]
8pub struct ClassSet {
9 pub classes: HashSet<String>,
11 pub responsive: HashMap<Breakpoint, HashSet<String>>,
13 pub conditional: HashMap<String, HashSet<String>>,
15 pub custom: HashMap<String, String>,
17}
18
19impl ClassSet {
20 pub fn new() -> Self {
22 Self {
23 classes: HashSet::new(),
24 responsive: HashMap::new(),
25 conditional: HashMap::new(),
26 custom: HashMap::new(),
27 }
28 }
29
30 pub fn add_class(&mut self, class: impl Into<String>) {
32 self.classes.insert(class.into());
33 }
34
35 pub fn add_classes(&mut self, classes: impl IntoIterator<Item = String>) {
37 for class in classes {
38 self.classes.insert(class);
39 }
40 }
41
42 pub fn add_responsive_class(&mut self, breakpoint: Breakpoint, class: impl Into<String>) {
44 self.responsive
45 .entry(breakpoint)
46 .or_default()
47 .insert(class.into());
48 }
49
50 pub fn add_conditional_class(
52 &mut self,
53 condition: impl Into<String>,
54 class: impl Into<String>,
55 ) {
56 self.conditional
57 .entry(condition.into())
58 .or_default()
59 .insert(class.into());
60 }
61
62 pub fn add_custom(&mut self, property: impl Into<String>, value: impl Into<String>) {
64 self.custom.insert(property.into(), value.into());
65 }
66
67 pub fn remove_class(&mut self, class: &str) {
69 self.classes.remove(class);
70 }
71
72 pub fn has_class(&self, class: &str) -> bool {
74 self.classes.contains(class)
75 }
76
77 pub fn get_classes(&self) -> Vec<String> {
79 self.classes.iter().cloned().collect()
80 }
81
82 pub fn get_responsive_classes(&self, breakpoint: Breakpoint) -> Vec<String> {
84 self.responsive
85 .get(&breakpoint)
86 .map(|classes| classes.iter().cloned().collect())
87 .unwrap_or_default()
88 }
89
90 pub fn get_all_responsive_classes(&self) -> HashMap<Breakpoint, Vec<String>> {
92 self.responsive
93 .iter()
94 .map(|(breakpoint, classes)| (*breakpoint, classes.iter().cloned().collect()))
95 .collect()
96 }
97
98 pub fn get_conditional_classes(&self, condition: &str) -> Vec<String> {
100 self.conditional
101 .get(condition)
102 .map(|classes| classes.iter().cloned().collect())
103 .unwrap_or_default()
104 }
105
106 pub fn get_all_conditional_classes(&self) -> HashMap<String, Vec<String>> {
108 self.conditional
109 .iter()
110 .map(|(condition, classes)| (condition.clone(), classes.iter().cloned().collect()))
111 .collect()
112 }
113
114 pub fn get_custom_properties(&self) -> HashMap<String, String> {
116 self.custom.clone()
117 }
118
119 pub fn to_css_classes(&self) -> String {
121 let mut result = Vec::new();
122
123 let mut base_classes: Vec<String> = self.classes.iter().cloned().collect();
125 base_classes.sort();
126 result.extend(base_classes);
127
128 let mut responsive_classes: Vec<(Breakpoint, String)> = self
130 .responsive
131 .iter()
132 .flat_map(|(breakpoint, classes)| {
133 classes
134 .iter()
135 .map(|class| (*breakpoint, format!("{}{}", breakpoint.prefix(), class)))
136 })
137 .collect();
138 responsive_classes.sort_by(|a, b| a.0.min_width().cmp(&b.0.min_width()));
139 result.extend(responsive_classes.into_iter().map(|(_, class)| class));
140
141 let mut custom_variant_classes: Vec<String> = self
143 .conditional
144 .iter()
145 .flat_map(|(variant, classes)| {
146 let variant = variant.clone();
147 classes
148 .iter()
149 .map(move |class| format!("{}:{}", variant, class))
150 })
151 .collect();
152 custom_variant_classes.sort();
153 result.extend(custom_variant_classes);
154
155 result.join(" ")
156 }
157
158 pub fn to_css_custom_properties(&self) -> String {
160 if self.custom.is_empty() {
161 return String::new();
162 }
163
164 let properties: Vec<String> = self
165 .custom
166 .iter()
167 .map(|(property, value)| format!("--{}: {}", property, value))
168 .collect();
169
170 format!("style=\"{}\"", properties.join("; "))
171 }
172
173 pub fn merge(&mut self, other: ClassSet) {
175 self.classes.extend(other.classes);
176
177 for (breakpoint, classes) in other.responsive {
178 self.responsive
179 .entry(breakpoint)
180 .or_default()
181 .extend(classes);
182 }
183
184 for (condition, classes) in other.conditional {
185 self.conditional
186 .entry(condition)
187 .or_default()
188 .extend(classes);
189 }
190
191 self.custom.extend(other.custom);
192 }
193
194 pub fn is_empty(&self) -> bool {
196 self.classes.is_empty()
197 && self.responsive.is_empty()
198 && self.conditional.is_empty()
199 && self.custom.is_empty()
200 }
201
202 pub fn len(&self) -> usize {
204 self.classes.len()
205 + self
206 .responsive
207 .values()
208 .map(|classes| classes.len())
209 .sum::<usize>()
210 + self
211 .conditional
212 .values()
213 .map(|classes| classes.len())
214 .sum::<usize>()
215 }
216}
217
218impl Default for ClassSet {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct ClassBuilder {
227 class_set: ClassSet,
228}
229
230impl ClassBuilder {
231 pub fn new() -> Self {
233 Self {
234 class_set: ClassSet::new(),
235 }
236 }
237
238 pub fn class(mut self, class: impl Into<String>) -> Self {
240 self.class_set.add_class(class);
241 self
242 }
243
244 pub fn classes(mut self, classes: impl IntoIterator<Item = String>) -> Self {
246 self.class_set.add_classes(classes);
247 self
248 }
249
250 pub fn responsive(mut self, breakpoint: Breakpoint, class: impl Into<String>) -> Self {
252 self.class_set.add_responsive_class(breakpoint, class);
253 self
254 }
255
256 pub fn conditional(mut self, condition: impl Into<String>, class: impl Into<String>) -> Self {
258 self.class_set.add_conditional_class(condition, class);
259 self
260 }
261
262 pub fn custom(mut self, property: impl Into<String>, value: impl Into<String>) -> Self {
264 self.class_set.add_custom(property, value);
265 self
266 }
267
268 pub fn custom_variant(mut self, variant: impl Into<String>, class: impl Into<String>) -> Self {
270 let variant = variant.into();
271 let class = class.into();
272
273 self.class_set.add_conditional_class(variant, class);
275 self
276 }
277
278 pub fn aria(self, aria_attr: impl Into<String>, class: impl Into<String>) -> Self {
280 let variant = format!("aria-{}", aria_attr.into());
281 self.custom_variant(variant, class)
282 }
283
284 pub fn data(self, data_attr: impl Into<String>, value: Option<String>, class: impl Into<String>) -> Self {
286 let variant = if let Some(val) = value {
287 format!("data-{}={}", data_attr.into(), val)
288 } else {
289 format!("data-{}", data_attr.into())
290 };
291 self.custom_variant(variant, class)
292 }
293
294 pub fn supports(self, feature: impl Into<String>, class: impl Into<String>) -> Self {
296 let variant = format!("supports-{}", feature.into());
297 self.custom_variant(variant, class)
298 }
299
300 pub fn build(self) -> ClassSet {
302 self.class_set
303 }
304
305 pub fn build_string(self) -> String {
307 self.class_set.to_css_classes()
308 }
309}
310
311impl Default for ClassBuilder {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317#[allow(clippy::module_inception)]
319pub mod classes {
320 use super::*;
321
322 pub fn new(classes: impl IntoIterator<Item = String>) -> ClassSet {
324 let mut class_set = ClassSet::new();
325 class_set.add_classes(classes);
326 class_set
327 }
328
329 pub fn responsive(
331 base: impl IntoIterator<Item = String>,
332 responsive: impl IntoIterator<Item = (Breakpoint, String)>,
333 ) -> ClassSet {
334 let mut class_set = ClassSet::new();
335 class_set.add_classes(base);
336
337 for (breakpoint, class) in responsive {
338 class_set.add_responsive_class(breakpoint, class);
339 }
340
341 class_set
342 }
343
344 pub fn conditional(
346 base: impl IntoIterator<Item = String>,
347 conditional: impl IntoIterator<Item = (String, String)>,
348 ) -> ClassSet {
349 let mut class_set = ClassSet::new();
350 class_set.add_classes(base);
351
352 for (condition, class) in conditional {
353 class_set.add_conditional_class(condition, class);
354 }
355
356 class_set
357 }
358
359 pub fn merge(class_sets: impl IntoIterator<Item = ClassSet>) -> ClassSet {
361 let mut result = ClassSet::new();
362
363 for class_set in class_sets {
364 result.merge(class_set);
365 }
366
367 result
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn test_class_set_creation() {
377 let class_set = ClassSet::new();
378 assert!(class_set.is_empty());
379 assert_eq!(class_set.len(), 0);
380 }
381
382 #[test]
383 fn test_class_set_add_remove() {
384 let mut class_set = ClassSet::new();
385
386 class_set.add_class("bg-blue-500");
387 assert!(class_set.has_class("bg-blue-500"));
388 assert_eq!(class_set.len(), 1);
389
390 class_set.add_class("text-white");
391 assert!(class_set.has_class("text-white"));
392 assert_eq!(class_set.len(), 2);
393
394 class_set.remove_class("bg-blue-500");
395 assert!(!class_set.has_class("bg-blue-500"));
396 assert!(class_set.has_class("text-white"));
397 assert_eq!(class_set.len(), 1);
398 }
399
400 #[test]
401 fn test_class_set_responsive() {
402 let mut class_set = ClassSet::new();
403
404 class_set.add_responsive_class(Breakpoint::Sm, "text-sm");
405 class_set.add_responsive_class(Breakpoint::Md, "text-md");
406
407 let sm_classes = class_set.get_responsive_classes(Breakpoint::Sm);
408 assert_eq!(sm_classes, vec!["text-sm"]);
409
410 let md_classes = class_set.get_responsive_classes(Breakpoint::Md);
411 assert_eq!(md_classes, vec!["text-md"]);
412
413 let lg_classes = class_set.get_responsive_classes(Breakpoint::Lg);
414 assert!(lg_classes.is_empty());
415 }
416
417 #[test]
418 fn test_class_set_conditional() {
419 let mut class_set = ClassSet::new();
420
421 class_set.add_conditional_class("hover", "hover:bg-blue-600");
422 class_set.add_conditional_class("focus", "focus:ring-2");
423
424 let hover_classes = class_set.get_conditional_classes("hover");
425 assert_eq!(hover_classes, vec!["hover:bg-blue-600"]);
426
427 let focus_classes = class_set.get_conditional_classes("focus");
428 assert_eq!(focus_classes, vec!["focus:ring-2"]);
429 }
430
431 #[test]
432 fn test_class_set_custom() {
433 let mut class_set = ClassSet::new();
434
435 class_set.add_custom("primary-color", "#3b82f6");
436 class_set.add_custom("spacing", "1rem");
437
438 let custom_properties = class_set.get_custom_properties();
439 assert_eq!(
440 custom_properties.get("primary-color"),
441 Some(&"#3b82f6".to_string())
442 );
443 assert_eq!(custom_properties.get("spacing"), Some(&"1rem".to_string()));
444 }
445
446 #[test]
447 fn test_class_set_to_css() {
448 let mut class_set = ClassSet::new();
449 class_set.add_class("bg-blue-500");
450 class_set.add_class("text-white");
451 class_set.add_responsive_class(Breakpoint::Sm, "text-sm");
452 class_set.add_responsive_class(Breakpoint::Md, "text-md");
453
454 let css = class_set.to_css_classes();
455 assert!(css.contains("bg-blue-500"));
456 assert!(css.contains("text-white"));
457 assert!(css.contains("sm:text-sm"));
459 assert!(css.contains("md:text-md"));
460 }
461
462 #[test]
463 fn test_class_set_merge() {
464 let mut class_set1 = ClassSet::new();
465 class_set1.add_class("bg-blue-500");
466 class_set1.add_responsive_class(Breakpoint::Sm, "text-sm");
467
468 let mut class_set2 = ClassSet::new();
469 class_set2.add_class("text-white");
470 class_set2.add_responsive_class(Breakpoint::Md, "text-md");
471
472 class_set1.merge(class_set2);
473
474 assert!(class_set1.has_class("bg-blue-500"));
475 assert!(class_set1.has_class("text-white"));
476 assert_eq!(
477 class_set1.get_responsive_classes(Breakpoint::Sm),
478 vec!["text-sm"]
479 );
480 assert_eq!(
481 class_set1.get_responsive_classes(Breakpoint::Md),
482 vec!["text-md"]
483 );
484 }
485
486 #[test]
487 fn test_class_builder() {
488 let class_set = ClassBuilder::new()
489 .class("bg-blue-500")
490 .class("text-white")
491 .responsive(Breakpoint::Sm, "text-sm")
492 .conditional("hover", "hover:bg-blue-600")
493 .custom("primary-color", "#3b82f6")
494 .build();
495
496 assert!(class_set.has_class("bg-blue-500"));
497 assert!(class_set.has_class("text-white"));
498 assert_eq!(
499 class_set.get_responsive_classes(Breakpoint::Sm),
500 vec!["text-sm"]
501 );
502 assert_eq!(
503 class_set.get_conditional_classes("hover"),
504 vec!["hover:bg-blue-600"]
505 );
506 assert_eq!(
507 class_set.get_custom_properties().get("primary-color"),
508 Some(&"#3b82f6".to_string())
509 );
510 }
511
512 #[test]
513 fn test_classes_utility_functions() {
514 let class_set = classes::new(vec!["bg-blue-500".to_string(), "text-white".to_string()]);
515 assert!(class_set.has_class("bg-blue-500"));
516 assert!(class_set.has_class("text-white"));
517
518 let responsive_class_set = classes::responsive(
519 vec!["bg-blue-500".to_string()],
520 vec![(Breakpoint::Sm, "text-sm".to_string())],
521 );
522 assert!(responsive_class_set.has_class("bg-blue-500"));
523 assert_eq!(
524 responsive_class_set.get_responsive_classes(Breakpoint::Sm),
525 vec!["text-sm"]
526 );
527
528 let conditional_class_set = classes::conditional(
529 vec!["bg-blue-500".to_string()],
530 vec![("hover".to_string(), "hover:bg-blue-600".to_string())],
531 );
532 assert!(conditional_class_set.has_class("bg-blue-500"));
533 assert_eq!(
534 conditional_class_set.get_conditional_classes("hover"),
535 vec!["hover:bg-blue-600"]
536 );
537 }
538}