headless_engine/render/
css.rs1#[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(), 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()); 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(); 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 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}