Skip to main content

headless_engine/render/
css.rs

1#[derive(Debug, Clone)]
2pub struct ComputedStyle {
3    pub display: Display,
4    pub color: String,
5    pub background_color: Option<String>,
6    pub font_size: f32,
7    pub font_weight: FontWeight,
8    pub width: Option<f32>,
9    pub height: Option<f32>,
10    pub margin_top: f32,
11    pub margin_bottom: f32,
12    pub margin_left: f32,
13    pub margin_right: f32,
14    pub padding_top: f32,
15    pub padding_bottom: f32,
16    pub padding_left: f32,
17    pub padding_right: f32,
18    pub border_radius: f32,
19    pub border_color: Option<String>,
20    pub border_width: f32,
21    pub flex_direction: FlexDirection,
22    pub gap: f32,
23    pub is_hidden: bool,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Display {
28    Block,
29    Inline,
30    InlineBlock,
31    Flex,
32    None,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum FontWeight {
37    Normal,
38    Bold,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum FlexDirection {
43    Row,
44    Column,
45}
46
47impl Default for ComputedStyle {
48    fn default() -> Self {
49        Self {
50            display: Display::Block,
51            color: "#e2e8f0".to_string(), // Slate-200 default
52            background_color: None,
53            font_size: 14.0,
54            font_weight: FontWeight::Normal,
55            width: None,
56            height: None,
57            margin_top: 0.0,
58            margin_bottom: 0.0,
59            margin_left: 0.0,
60            margin_right: 0.0,
61            padding_top: 0.0,
62            padding_bottom: 0.0,
63            padding_left: 0.0,
64            padding_right: 0.0,
65            border_radius: 0.0,
66            border_color: None,
67            border_width: 0.0,
68            flex_direction: FlexDirection::Row,
69            gap: 0.0,
70            is_hidden: false,
71        }
72    }
73}
74
75pub struct CssParser;
76
77impl CssParser {
78    pub fn default_style_for_tag(tag: &str) -> ComputedStyle {
79        let mut s = ComputedStyle::default();
80        match tag {
81            "html" | "body" => {
82                s.display = Display::Block;
83                s.background_color = Some("#0f172a".to_string()); // Default dark slate theme
84                s.color = "#f8fafc".to_string();
85                s.font_size = 14.0;
86            }
87            "h1" => {
88                s.display = Display::Block;
89                s.font_size = 26.0;
90                s.font_weight = FontWeight::Bold;
91                s.color = "#f8fafc".to_string();
92                s.margin_top = 16.0;
93                s.margin_bottom = 12.0;
94            }
95            "h2" => {
96                s.display = Display::Block;
97                s.font_size = 20.0;
98                s.font_weight = FontWeight::Bold;
99                s.color = "#38bdf8".to_string();
100                s.margin_top = 14.0;
101                s.margin_bottom = 8.0;
102            }
103            "h3" => {
104                s.display = Display::Block;
105                s.font_size = 16.0;
106                s.font_weight = FontWeight::Bold;
107                s.color = "#e2e8f0".to_string();
108                s.margin_top = 10.0;
109                s.margin_bottom = 6.0;
110            }
111            "p" => {
112                s.display = Display::Block;
113                s.font_size = 14.0;
114                s.color = "#cbd5e1".to_string();
115                s.margin_bottom = 10.0;
116            }
117            "a" => {
118                s.display = Display::Inline;
119                s.color = "#60a5fa".to_string(); // Blue link
120                s.font_size = 14.0;
121            }
122            "button" => {
123                s.display = Display::InlineBlock;
124                s.background_color = Some("#3b82f6".to_string());
125                s.color = "#ffffff".to_string();
126                s.font_weight = FontWeight::Bold;
127                s.padding_top = 6.0;
128                s.padding_bottom = 6.0;
129                s.padding_left = 14.0;
130                s.padding_right = 14.0;
131                s.border_radius = 6.0;
132                s.margin_right = 8.0;
133                s.margin_bottom = 8.0;
134            }
135            "input" | "textarea" => {
136                s.display = Display::InlineBlock;
137                s.background_color = Some("#1e293b".to_string());
138                s.color = "#f8fafc".to_string();
139                s.border_color = Some("#475569".to_string());
140                s.border_width = 1.0;
141                s.border_radius = 6.0;
142                s.padding_top = 6.0;
143                s.padding_bottom = 6.0;
144                s.padding_left = 10.0;
145                s.padding_right = 10.0;
146                s.width = Some(240.0);
147                s.margin_right = 8.0;
148                s.margin_bottom = 8.0;
149            }
150            "img" => {
151                s.display = Display::InlineBlock;
152                s.border_radius = 6.0;
153            }
154            "header" | "nav" => {
155                s.display = Display::Block;
156                s.background_color = Some("#1e293b".to_string());
157                s.padding_top = 12.0;
158                s.padding_bottom = 12.0;
159                s.padding_left = 16.0;
160                s.padding_right = 16.0;
161                s.margin_bottom = 16.0;
162            }
163            "table" => {
164                s.display = Display::Block;
165                s.margin_top = 10.0;
166                s.margin_bottom = 14.0;
167                s.border_width = 1.0;
168                s.border_color = Some("#334155".to_string());
169            }
170            "tr" => {
171                s.display = Display::Flex;
172                s.flex_direction = FlexDirection::Row;
173            }
174            "th" | "td" => {
175                s.display = Display::Block;
176                s.padding_top = 6.0;
177                s.padding_bottom = 6.0;
178                s.padding_left = 10.0;
179                s.padding_right = 10.0;
180                s.border_width = 1.0;
181                s.border_color = Some("#334155".to_string());
182            }
183            "script" | "style" | "noscript" | "meta" | "head" | "link" => {
184                s.display = Display::None;
185                s.is_hidden = true;
186            }
187            _ => {
188                // span, div, section, article, etc.
189                s.display = if tag == "span" || tag == "b" || tag == "i" || tag == "strong" {
190                    Display::Inline
191                } else {
192                    Display::Block
193                };
194            }
195        }
196        s
197    }
198
199    pub fn parse_inline_style(style_str: &str, base_style: &mut ComputedStyle) {
200        for rule in style_str.split(';') {
201            let rule = rule.trim();
202            if rule.is_empty() {
203                continue;
204            }
205            let mut parts = rule.splitn(2, ':');
206            let prop = parts.next().unwrap_or("").trim().to_lowercase();
207            let val = parts.next().unwrap_or("").trim();
208
209            match prop.as_str() {
210                "display" => match val {
211                    "none" => {
212                        base_style.display = Display::None;
213                        base_style.is_hidden = true;
214                    }
215                    "flex" => base_style.display = Display::Flex,
216                    "inline" => base_style.display = Display::Inline,
217                    "inline-block" => base_style.display = Display::InlineBlock,
218                    "block" => base_style.display = Display::Block,
219                    _ => {}
220                },
221                "color" => {
222                    base_style.color = Self::parse_color(val);
223                }
224                "background-color" | "background" => {
225                    if !val.contains("url") {
226                        base_style.background_color = Some(Self::parse_color(val));
227                    }
228                }
229                "font-size" => {
230                    if let Some(px) = Self::parse_pixels(val) {
231                        base_style.font_size = px;
232                    }
233                }
234                "font-weight" => {
235                    if val == "bold" || val == "700" || val == "800" || val == "900" {
236                        base_style.font_weight = FontWeight::Bold;
237                    } else {
238                        base_style.font_weight = FontWeight::Normal;
239                    }
240                }
241                "width" => {
242                    base_style.width = Self::parse_pixels(val);
243                }
244                "height" => {
245                    base_style.height = Self::parse_pixels(val);
246                }
247                "margin" => {
248                    if let Some(px) = Self::parse_pixels(val) {
249                        base_style.margin_top = px;
250                        base_style.margin_bottom = px;
251                        base_style.margin_left = px;
252                        base_style.margin_right = px;
253                    }
254                }
255                "padding" => {
256                    if let Some(px) = Self::parse_pixels(val) {
257                        base_style.padding_top = px;
258                        base_style.padding_bottom = px;
259                        base_style.padding_left = px;
260                        base_style.padding_right = px;
261                    }
262                }
263                "border-radius" => {
264                    if let Some(px) = Self::parse_pixels(val) {
265                        base_style.border_radius = px;
266                    }
267                }
268                "border" => {
269                    base_style.border_width = 1.0;
270                    base_style.border_color = Some("#475569".to_string());
271                }
272                "flex-direction" => {
273                    if val == "column" {
274                        base_style.flex_direction = FlexDirection::Column;
275                    } else {
276                        base_style.flex_direction = FlexDirection::Row;
277                    }
278                }
279                "gap" => {
280                    if let Some(px) = Self::parse_pixels(val) {
281                        base_style.gap = px;
282                    }
283                }
284                _ => {}
285            }
286        }
287    }
288
289    fn parse_pixels(val: &str) -> Option<f32> {
290        let clean = val
291            .trim()
292            .trim_end_matches("px")
293            .trim_end_matches("em")
294            .trim_end_matches("rem");
295        clean.parse::<f32>().ok()
296    }
297
298    fn parse_color(val: &str) -> String {
299        let val = val.trim();
300        if val.starts_with('#') || val.starts_with("rgb") || val.starts_with("hsl") {
301            val.to_string()
302        } else {
303            match val {
304                "white" => "#ffffff".to_string(),
305                "black" => "#000000".to_string(),
306                "red" => "#ef4444".to_string(),
307                "blue" => "#3b82f6".to_string(),
308                "green" => "#10b981".to_string(),
309                "gray" | "grey" => "#64748b".to_string(),
310                _ => val.to_string(),
311            }
312        }
313    }
314}