tailwind_rs_core/classes/
class_set.rs

1//! ClassSet implementation
2//!
3//! This module contains the ClassSet struct and its methods.
4
5use crate::responsive::Breakpoint;
6use std::collections::{HashMap, HashSet};
7
8/// A set of CSS classes with metadata
9#[derive(Debug, Clone, PartialEq)]
10pub struct ClassSet {
11    /// The actual CSS classes
12    pub classes: HashSet<String>,
13    /// Responsive classes organized by breakpoint
14    pub responsive: HashMap<Breakpoint, HashSet<String>>,
15    /// Conditional classes
16    pub conditional: HashMap<String, HashSet<String>>,
17    /// Custom CSS properties
18    pub custom: HashMap<String, String>,
19}
20
21impl ClassSet {
22    /// Create a new empty class set
23    pub fn new() -> Self {
24        Self {
25            classes: HashSet::new(),
26            responsive: HashMap::new(),
27            conditional: HashMap::new(),
28            custom: HashMap::new(),
29        }
30    }
31
32    /// Add a base class
33    pub fn add_class(&mut self, class: impl Into<String>) {
34        self.classes.insert(class.into());
35    }
36
37    /// Add multiple base classes
38    pub fn add_classes(&mut self, classes: impl IntoIterator<Item = String>) {
39        for class in classes {
40            self.classes.insert(class);
41        }
42    }
43
44    /// Add a responsive class
45    pub fn add_responsive_class(&mut self, breakpoint: Breakpoint, class: impl Into<String>) {
46        self.responsive
47            .entry(breakpoint)
48            .or_default()
49            .insert(class.into());
50    }
51
52    /// Add a conditional class
53    pub fn add_conditional_class(
54        &mut self,
55        condition: impl Into<String>,
56        class: impl Into<String>,
57    ) {
58        self.conditional
59            .entry(condition.into())
60            .or_default()
61            .insert(class.into());
62    }
63
64    /// Add a custom CSS property
65    pub fn add_custom(&mut self, property: impl Into<String>, value: impl Into<String>) {
66        self.custom.insert(property.into(), value.into());
67    }
68
69    /// Remove a base class
70    pub fn remove_class(&mut self, class: &str) {
71        self.classes.remove(class);
72    }
73
74    /// Check if a base class exists
75    pub fn has_class(&self, class: &str) -> bool {
76        self.classes.contains(class)
77    }
78
79    /// Get all base classes as a vector
80    pub fn get_classes(&self) -> Vec<String> {
81        self.classes.iter().cloned().collect()
82    }
83
84    /// Get responsive classes for a specific breakpoint
85    pub fn get_responsive_classes(&self, breakpoint: Breakpoint) -> Vec<String> {
86        self.responsive
87            .get(&breakpoint)
88            .map(|classes| classes.iter().cloned().collect())
89            .unwrap_or_default()
90    }
91
92    /// Get all responsive classes
93    pub fn get_all_responsive_classes(&self) -> HashMap<Breakpoint, Vec<String>> {
94        self.responsive
95            .iter()
96            .map(|(breakpoint, classes)| (*breakpoint, classes.iter().cloned().collect()))
97            .collect()
98    }
99
100    /// Get conditional classes for a specific condition
101    pub fn get_conditional_classes(&self, condition: &str) -> Vec<String> {
102        self.conditional
103            .get(condition)
104            .map(|classes| classes.iter().cloned().collect())
105            .unwrap_or_default()
106    }
107
108    /// Get all conditional classes
109    pub fn get_all_conditional_classes(&self) -> HashMap<String, Vec<String>> {
110        self.conditional
111            .iter()
112            .map(|(condition, classes)| (condition.clone(), classes.iter().cloned().collect()))
113            .collect()
114    }
115
116    /// Get custom CSS properties
117    pub fn get_custom_properties(&self) -> HashMap<String, String> {
118        self.custom.clone()
119    }
120
121    /// Convert to CSS class string
122    pub fn to_css_classes(&self) -> String {
123        let mut result = Vec::new();
124
125        // Add base classes
126        let mut base_classes: Vec<String> = self.classes.iter().cloned().collect();
127        base_classes.sort();
128        result.extend(base_classes);
129
130        // Add responsive classes
131        let mut responsive_classes: Vec<(Breakpoint, String)> = self
132            .responsive
133            .iter()
134            .flat_map(|(breakpoint, classes)| {
135                classes
136                    .iter()
137                    .map(|class| (*breakpoint, format!("{}{}", breakpoint.prefix(), class)))
138            })
139            .collect();
140        responsive_classes.sort_by(|a, b| a.0.min_width().cmp(&b.0.min_width()));
141        result.extend(responsive_classes.into_iter().map(|(_, class)| class));
142
143        // Add custom variant classes (Tailwind v4.1.13 @custom-variant support)
144        let mut custom_variant_classes: Vec<String> = self
145            .conditional
146            .iter()
147            .flat_map(|(variant, classes)| {
148                let variant = variant.clone();
149                classes
150                    .iter()
151                    .map(move |class| format!("{}:{}", variant, class))
152            })
153            .collect();
154        custom_variant_classes.sort();
155        result.extend(custom_variant_classes);
156
157        result.join(" ")
158    }
159
160    /// Convert to CSS custom properties string
161    pub fn to_css_custom_properties(&self) -> String {
162        if self.custom.is_empty() {
163            return String::new();
164        }
165
166        let properties: Vec<String> = self
167            .custom
168            .iter()
169            .map(|(property, value)| format!("--{}: {}", property, value))
170            .collect();
171
172        format!("style=\"{}\"", properties.join("; "))
173    }
174
175    /// Merge another class set into this one
176    pub fn merge(&mut self, other: ClassSet) {
177        self.classes.extend(other.classes);
178
179        for (breakpoint, classes) in other.responsive {
180            self.responsive
181                .entry(breakpoint)
182                .or_default()
183                .extend(classes);
184        }
185
186        for (condition, classes) in other.conditional {
187            self.conditional
188                .entry(condition)
189                .or_default()
190                .extend(classes);
191        }
192
193        self.custom.extend(other.custom);
194    }
195
196    /// Check if the class set is empty
197    pub fn is_empty(&self) -> bool {
198        self.classes.is_empty()
199            && self.responsive.is_empty()
200            && self.conditional.is_empty()
201            && self.custom.is_empty()
202    }
203
204    /// Get the total number of classes
205    pub fn len(&self) -> usize {
206        self.classes.len()
207            + self.responsive.values().map(|classes| classes.len()).sum::<usize>()
208            + self.conditional.values().map(|classes| classes.len()).sum::<usize>()
209    }
210}
211
212impl Default for ClassSet {
213    fn default() -> Self {
214        Self::new()
215    }
216}