Skip to main content

oak_tailwind/engine/
mod.rs

1use dashmap::DashMap;
2use lazy_static::lazy_static;
3
4lazy_static! {
5    static ref STATIC_RULES: DashMap<&'static str, &'static str> = {
6        let m = DashMap::new();
7        // Layout
8        m.insert("flex", "display: flex;");
9        m.insert("items-center", "align-items: center;");
10        m.insert("justify-center", "justify-content: center;");
11        m.insert("block", "display: block;");
12        m.insert("inline-block", "display: inline-block;");
13        m.insert("hidden", "display: none;");
14
15        // Colors (Basic)
16        m.insert("bg-white", "background-color: #ffffff;");
17        m.insert("bg-black", "background-color: #000000;");
18        m.insert("bg-blue", "background-color: #0000ff;"); // Match tests
19        m.insert("text-white", "color: #ffffff;");
20        m.insert("text-black", "color: #000000;");
21
22        // Typography
23        m.insert("font-bold", "font-weight: 700;");
24        m.insert("text-center", "text-align: center;");
25
26        // Shadows
27        m.insert("shadow-sm", "box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);");
28
29        m
30    };
31
32    static ref GENERATED_CACHE: DashMap<String, String> = DashMap::new();
33}
34
35pub struct TailwindEngine;
36
37impl TailwindEngine {
38    pub fn new() -> Self {
39        Self
40    }
41
42    pub fn generate_css(&self, classes: &str) -> String {
43        let mut css = String::with_capacity(classes.len() * 4);
44        let mut seen = std::collections::HashSet::new();
45
46        for class in classes.split_whitespace() {
47            if !seen.insert(class) {
48                continue;
49            }
50
51            // 1. Check cache first
52            if let Some(rules) = GENERATED_CACHE.get(class) {
53                css.push_str(&rules);
54                continue;
55            }
56
57            // 2. Check static rules
58            if let Some(rules) = STATIC_RULES.get(class) {
59                let rule_str = format!(".{} {{ {} }}\n", escape_class(class), *rules);
60                css.push_str(&rule_str);
61                GENERATED_CACHE.insert(class.to_string(), rule_str);
62                continue;
63            }
64
65            // 3. Check dynamic patterns
66            if let Some(rules) = self.match_dynamic(class) {
67                let rule_str = format!(".{} {{ {} }}\n", escape_class(class), rules);
68                css.push_str(&rule_str);
69                GENERATED_CACHE.insert(class.to_string(), rule_str);
70                continue;
71            }
72        }
73        css
74    }
75
76    fn match_dynamic(&self, class: &str) -> Option<String> {
77        // Spacing: p-4, m-2, mx-4, pt-1 etc.
78        if (class.starts_with('p') || class.starts_with('m')) && class.contains('-') {
79            let parts: Vec<&str> = class.split('-').collect();
80            if parts.len() == 2 {
81                let prefix_dir = parts[0];
82                let val_str = parts[1];
83
84                if prefix_dir.len() >= 1 && prefix_dir.len() <= 2 {
85                    let prefix = &prefix_dir[0..1];
86                    let dir = &prefix_dir[1..];
87
88                    if let Ok(val_num) = val_str.parse::<f32>() {
89                        let val = val_num * 0.25;
90                        let prop = if prefix == "p" { "padding" } else { "margin" };
91                        return Some(match dir {
92                            "x" => format!("{}-left: {}rem; {}-right: {}rem;", prop, val, prop, val),
93                            "y" => format!("{}-top: {}rem; {}-bottom: {}rem;", prop, val, prop, val),
94                            "t" => format!("{}-top: {}rem;", prop, val),
95                            "r" => format!("{}-right: {}rem;", prop, val),
96                            "b" => format!("{}-bottom: {}rem;", prop, val),
97                            "l" => format!("{}-left: {}rem;", prop, val),
98                            "" => format!("{}: {}rem;", prop, val),
99                            _ => return None,
100                        });
101                    }
102                }
103            }
104        }
105
106        // Text sizes: text-2xl, text-xl, etc.
107        if class.starts_with("text-") {
108            let size_part = &class[5..];
109            let size = match size_part {
110                "xs" => "0.75rem",
111                "sm" => "0.875rem",
112                "base" => "1rem",
113                "lg" => "1.125rem",
114                "xl" => "1.25rem",
115                "2xl" => "1.5rem",
116                "3xl" => "1.875rem",
117                "4xl" => "2.25rem",
118                _ => return None,
119            };
120            return Some(format!("font-size: {};", size));
121        }
122
123        // Colors: bg-blue-500, text-red-500
124        if (class.starts_with("bg-") || class.starts_with("text-") || class.starts_with("border-")) && class.matches('-').count() == 2 {
125            let parts: Vec<&str> = class.split('-').collect();
126            if parts.len() == 3 {
127                let prop_prefix = parts[0];
128                let color = parts[1];
129                let shade = parts[2];
130
131                let prop = match prop_prefix {
132                    "bg" => "background-color",
133                    "text" => "color",
134                    "border" => "border-color",
135                    _ => return None,
136                };
137
138                let hex = match (color, shade) {
139                    ("blue", "500") => "#3b82f6",
140                    ("red", "500") => "#ef4444",
141                    ("green", "500") => "#22c55e",
142                    ("gray", "500") => "#6b7280",
143                    ("yellow", "500") => "#eab308",
144                    _ => return None,
145                };
146                return Some(format!("{}: {};", prop, hex));
147            }
148        }
149
150        // Borders: rounded-lg, border-2
151        if class.starts_with("rounded") {
152            let suffix = if class == "rounded" { "" } else { &class[7..] };
153            let radius = match suffix {
154                "-sm" => "0.125rem",
155                "-md" => "0.375rem",
156                "-lg" => "0.5rem",
157                "-full" => "9999px",
158                "" => "0.25rem",
159                _ => return None,
160            };
161            return Some(format!("border-radius: {};", radius));
162        }
163
164        None
165    }
166}
167
168fn escape_class(class: &str) -> String {
169    // Basic CSS class escaping
170    class.replace(':', "\\:").replace('[', "\\[").replace(']', "\\]").replace('.', "\\.")
171}