tailwind_rs_core/classes/
class_set.rs

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