1use crate::error::{Result, TailwindError};
7use crate::responsive::Breakpoint;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq)]
12pub struct CssRule {
13 pub selector: String,
15 pub properties: Vec<CssProperty>,
17 pub media_query: Option<String>,
19 pub specificity: u32,
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct CssProperty {
26 pub name: String,
28 pub value: String,
30 pub important: bool,
32}
33
34#[derive(Debug, Clone)]
36pub struct CssGenerator {
37 rules: HashMap<String, CssRule>,
39 breakpoints: HashMap<Breakpoint, String>,
41 custom_properties: HashMap<String, String>,
43}
44
45impl CssGenerator {
46 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 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 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 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; let responsive_class = format!("{}:{}", breakpoint.prefix().trim_end_matches(':'), class);
79 self.rules.insert(responsive_class, rule);
80 Ok(())
81 }
82
83 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 pub fn generate_css(&self) -> String {
90 let mut css = String::new();
91
92 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 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 for rule in base_rules {
115 css.push_str(&self.rule_to_css(rule));
116 }
117
118 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 pub fn generate_minified_css(&self) -> String {
132 let css = self.generate_css();
133 self.minify_css(&css)
134 }
135
136 pub fn get_rules(&self) -> &HashMap<String, CssRule> {
138 &self.rules
139 }
140
141 pub fn rule_count(&self) -> usize {
143 self.rules.len()
144 }
145
146 pub fn remove_rule(&mut self, selector: &str) -> Option<CssRule> {
148 self.rules.remove(selector)
149 }
150
151 pub fn update_rule(&mut self, selector: &str, rule: CssRule) {
153 self.rules.insert(selector.to_string(), rule);
154 }
155
156 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 fn class_to_properties(&self, class: &str) -> Result<Vec<CssProperty>> {
171 match class {
172 "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 "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 "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-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 "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 "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 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 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}