tailwind_rs_core/
css_generator.rs

1//! CSS generation system for tailwind-rs
2//!
3//! This module provides the core CSS generation functionality, converting
4//! Tailwind class names into actual CSS rules.
5
6use crate::error::{Result, TailwindError};
7use crate::responsive::Breakpoint;
8use std::collections::HashMap;
9
10/// Represents a CSS rule with selector and properties
11#[derive(Debug, Clone, PartialEq)]
12pub struct CssRule {
13    /// CSS selector (e.g., ".p-4", ".md:bg-blue-500")
14    pub selector: String,
15    /// CSS properties for this rule
16    pub properties: Vec<CssProperty>,
17    /// Media query for responsive rules
18    pub media_query: Option<String>,
19    /// CSS specificity score
20    pub specificity: u32,
21}
22
23/// Represents a CSS property
24#[derive(Debug, Clone, PartialEq)]
25pub struct CssProperty {
26    /// Property name (e.g., "padding", "background-color")
27    pub name: String,
28    /// Property value (e.g., "1rem", "#3b82f6")
29    pub value: String,
30    /// Whether the property is marked as !important
31    pub important: bool,
32}
33
34/// CSS generator that converts Tailwind classes to CSS rules
35#[derive(Debug, Clone)]
36pub struct CssGenerator {
37    /// Generated CSS rules
38    rules: HashMap<String, CssRule>,
39    /// Responsive breakpoints
40    breakpoints: HashMap<Breakpoint, String>,
41    /// Custom CSS properties
42    custom_properties: HashMap<String, String>,
43}
44
45impl CssGenerator {
46    /// Create a new CSS generator
47    pub fn new() -> Self {
48        let mut generator = Self {
49            rules: HashMap::new(),
50            breakpoints: HashMap::new(),
51            custom_properties: HashMap::new(),
52        };
53        
54        // Initialize default breakpoints
55        generator.breakpoints.insert(Breakpoint::Sm, "(min-width: 640px)".to_string());
56        generator.breakpoints.insert(Breakpoint::Md, "(min-width: 768px)".to_string());
57        generator.breakpoints.insert(Breakpoint::Lg, "(min-width: 1024px)".to_string());
58        generator.breakpoints.insert(Breakpoint::Xl, "(min-width: 1280px)".to_string());
59        generator.breakpoints.insert(Breakpoint::Xl2, "(min-width: 1536px)".to_string());
60        
61        generator
62    }
63
64    /// Add a class to the generator
65    pub fn add_class(&mut self, class: &str) -> Result<()> {
66        let rule = self.class_to_css_rule(class)?;
67        self.rules.insert(class.to_string(), rule);
68        Ok(())
69    }
70
71    /// Add a responsive class
72    pub fn add_responsive_class(&mut self, breakpoint: Breakpoint, class: &str) -> Result<()> {
73        let mut rule = self.class_to_css_rule(class)?;
74        rule.selector = format!("{}{}", breakpoint.prefix(), class);
75        rule.media_query = self.breakpoints.get(&breakpoint).cloned();
76        rule.specificity = 20; // Higher specificity for responsive rules
77        
78        let responsive_class = format!("{}:{}", breakpoint.prefix().trim_end_matches(':'), class);
79        self.rules.insert(responsive_class, rule);
80        Ok(())
81    }
82
83    /// Add a custom CSS property
84    pub fn add_custom_property(&mut self, name: &str, value: &str) {
85        self.custom_properties.insert(name.to_string(), value.to_string());
86    }
87
88    /// Generate CSS from all added classes
89    pub fn generate_css(&self) -> String {
90        let mut css = String::new();
91        
92        // Add custom properties
93        if !self.custom_properties.is_empty() {
94            css.push_str(":root {\n");
95            for (name, value) in &self.custom_properties {
96                css.push_str(&format!("  --{}: {};\n", name, value));
97            }
98            css.push_str("}\n\n");
99        }
100        
101        // Group rules by media query
102        let mut base_rules = Vec::new();
103        let mut responsive_rules: HashMap<String, Vec<&CssRule>> = HashMap::new();
104        
105        for rule in self.rules.values() {
106            if let Some(ref media_query) = rule.media_query {
107                responsive_rules.entry(media_query.clone()).or_default().push(rule);
108            } else {
109                base_rules.push(rule);
110            }
111        }
112        
113        // Generate base rules
114        for rule in base_rules {
115            css.push_str(&self.rule_to_css(rule));
116        }
117        
118        // Generate responsive rules
119        for (media_query, rules) in responsive_rules {
120            css.push_str(&format!("@media {} {{\n", media_query));
121            for rule in rules {
122                css.push_str(&format!("  {}\n", self.rule_to_css(rule)));
123            }
124            css.push_str("}\n\n");
125        }
126        
127        css
128    }
129
130    /// Generate minified CSS
131    pub fn generate_minified_css(&self) -> String {
132        let css = self.generate_css();
133        self.minify_css(&css)
134    }
135
136    /// Get all generated rules
137    pub fn get_rules(&self) -> &HashMap<String, CssRule> {
138        &self.rules
139    }
140
141    /// Get the number of generated rules
142    pub fn rule_count(&self) -> usize {
143        self.rules.len()
144    }
145
146    /// Remove a CSS rule by selector
147    pub fn remove_rule(&mut self, selector: &str) -> Option<CssRule> {
148        self.rules.remove(selector)
149    }
150
151    /// Update a CSS rule
152    pub fn update_rule(&mut self, selector: &str, rule: CssRule) {
153        self.rules.insert(selector.to_string(), rule);
154    }
155
156    /// Convert a class name to a CSS rule
157    fn class_to_css_rule(&self, class: &str) -> Result<CssRule> {
158        let selector = format!(".{}", class);
159        let properties = self.class_to_properties(class)?;
160        
161        Ok(CssRule {
162            selector,
163            properties,
164            media_query: None,
165            specificity: 10,
166        })
167    }
168
169    /// Convert a class name to CSS properties
170    fn class_to_properties(&self, class: &str) -> Result<Vec<CssProperty>> {
171        match class {
172            // Spacing utilities
173            "p-0" => Ok(vec![CssProperty { name: "padding".to_string(), value: "0px".to_string(), important: false }]),
174            "p-1" => Ok(vec![CssProperty { name: "padding".to_string(), value: "0.25rem".to_string(), important: false }]),
175            "p-2" => Ok(vec![CssProperty { name: "padding".to_string(), value: "0.5rem".to_string(), important: false }]),
176            "p-3" => Ok(vec![CssProperty { name: "padding".to_string(), value: "0.75rem".to_string(), important: false }]),
177            "p-4" => Ok(vec![CssProperty { name: "padding".to_string(), value: "1rem".to_string(), important: false }]),
178            "p-5" => Ok(vec![CssProperty { name: "padding".to_string(), value: "1.25rem".to_string(), important: false }]),
179            "p-6" => Ok(vec![CssProperty { name: "padding".to_string(), value: "1.5rem".to_string(), important: false }]),
180            "p-8" => Ok(vec![CssProperty { name: "padding".to_string(), value: "2rem".to_string(), important: false }]),
181            
182            // Margin utilities
183            "m-0" => Ok(vec![CssProperty { name: "margin".to_string(), value: "0px".to_string(), important: false }]),
184            "m-1" => Ok(vec![CssProperty { name: "margin".to_string(), value: "0.25rem".to_string(), important: false }]),
185            "m-2" => Ok(vec![CssProperty { name: "margin".to_string(), value: "0.5rem".to_string(), important: false }]),
186            "m-3" => Ok(vec![CssProperty { name: "margin".to_string(), value: "0.75rem".to_string(), important: false }]),
187            "m-4" => Ok(vec![CssProperty { name: "margin".to_string(), value: "1rem".to_string(), important: false }]),
188            "m-5" => Ok(vec![CssProperty { name: "margin".to_string(), value: "1.25rem".to_string(), important: false }]),
189            "m-6" => Ok(vec![CssProperty { name: "margin".to_string(), value: "1.5rem".to_string(), important: false }]),
190            "m-8" => Ok(vec![CssProperty { name: "margin".to_string(), value: "2rem".to_string(), important: false }]),
191            
192            // Background colors
193            "bg-white" => Ok(vec![CssProperty { name: "background-color".to_string(), value: "#ffffff".to_string(), important: false }]),
194            "bg-black" => Ok(vec![CssProperty { name: "background-color".to_string(), value: "#000000".to_string(), important: false }]),
195            "bg-blue-500" => Ok(vec![CssProperty { name: "background-color".to_string(), value: "#3b82f6".to_string(), important: false }]),
196            "bg-red-500" => Ok(vec![CssProperty { name: "background-color".to_string(), value: "#ef4444".to_string(), important: false }]),
197            "bg-green-500" => Ok(vec![CssProperty { name: "background-color".to_string(), value: "#22c55e".to_string(), important: false }]),
198            
199            // Text colors
200            "text-white" => Ok(vec![CssProperty { name: "color".to_string(), value: "#ffffff".to_string(), important: false }]),
201            "text-black" => Ok(vec![CssProperty { name: "color".to_string(), value: "#000000".to_string(), important: false }]),
202            "text-blue-500" => Ok(vec![CssProperty { name: "color".to_string(), value: "#3b82f6".to_string(), important: false }]),
203            "text-red-500" => Ok(vec![CssProperty { name: "color".to_string(), value: "#ef4444".to_string(), important: false }]),
204            "text-green-500" => Ok(vec![CssProperty { name: "color".to_string(), value: "#22c55e".to_string(), important: false }]),
205            
206            // Display utilities
207            "block" => Ok(vec![CssProperty { name: "display".to_string(), value: "block".to_string(), important: false }]),
208            "inline" => Ok(vec![CssProperty { name: "display".to_string(), value: "inline".to_string(), important: false }]),
209            "flex" => Ok(vec![CssProperty { name: "display".to_string(), value: "flex".to_string(), important: false }]),
210            "grid" => Ok(vec![CssProperty { name: "display".to_string(), value: "grid".to_string(), important: false }]),
211            "hidden" => Ok(vec![CssProperty { name: "display".to_string(), value: "none".to_string(), important: false }]),
212            
213            // Border radius
214            "rounded-md" => Ok(vec![CssProperty { name: "border-radius".to_string(), value: "0.375rem".to_string(), important: false }]),
215            
216            _ => Err(TailwindError::class_generation(format!("Unknown class: {}", class))),
217        }
218    }
219
220    /// Convert a CSS rule to CSS string
221    fn rule_to_css(&self, rule: &CssRule) -> String {
222        let mut css = format!("{} {{\n", rule.selector);
223        for property in &rule.properties {
224            let important = if property.important { " !important" } else { "" };
225            css.push_str(&format!("  {}: {}{};\n", property.name, property.value, important));
226        }
227        css.push_str("}\n");
228        css
229    }
230
231    /// Minify CSS by removing unnecessary whitespace
232    fn minify_css(&self, css: &str) -> String {
233        css.lines()
234            .map(|line| line.trim())
235            .filter(|line| !line.is_empty())
236            .collect::<Vec<&str>>()
237            .join("")
238            .replace(" {", "{")
239            .replace("} ", "}")
240            .replace("; ", ";")
241            .replace(" ", "")
242    }
243}
244
245impl Default for CssGenerator {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_css_generator_creation() {
257        let generator = CssGenerator::new();
258        assert_eq!(generator.rule_count(), 0);
259        assert!(!generator.breakpoints.is_empty());
260    }
261
262    #[test]
263    fn test_add_class() {
264        let mut generator = CssGenerator::new();
265        generator.add_class("p-4").unwrap();
266        
267        assert_eq!(generator.rule_count(), 1);
268        let rules = generator.get_rules();
269        assert!(rules.contains_key("p-4"));
270    }
271
272    #[test]
273    fn test_generate_css() {
274        let mut generator = CssGenerator::new();
275        generator.add_class("p-4").unwrap();
276        generator.add_class("bg-blue-500").unwrap();
277        
278        let css = generator.generate_css();
279        assert!(css.contains(".p-4"));
280        assert!(css.contains("padding: 1rem"));
281        assert!(css.contains(".bg-blue-500"));
282        assert!(css.contains("background-color: #3b82f6"));
283    }
284
285    #[test]
286    fn test_responsive_class() {
287        let mut generator = CssGenerator::new();
288        generator.add_responsive_class(Breakpoint::Md, "p-4").unwrap();
289        
290        let css = generator.generate_css();
291        assert!(css.contains("@media (min-width: 768px)"));
292        assert!(css.contains("md:p-4"));
293    }
294
295    #[test]
296    fn test_custom_properties() {
297        let mut generator = CssGenerator::new();
298        generator.add_custom_property("primary-color", "#3b82f6");
299        
300        let css = generator.generate_css();
301        assert!(css.contains(":root"));
302        assert!(css.contains("--primary-color: #3b82f6"));
303    }
304
305    #[test]
306    fn test_minified_css() {
307        let mut generator = CssGenerator::new();
308        generator.add_class("p-4").unwrap();
309        
310        let minified = generator.generate_minified_css();
311        assert!(!minified.contains('\n'));
312        assert!(!minified.contains(' '));
313    }
314
315    #[test]
316    fn test_unknown_class() {
317        let mut generator = CssGenerator::new();
318        let result = generator.add_class("unknown-class");
319        assert!(result.is_err());
320    }
321}