1use crate::StyleSheet;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Default)]
8pub struct ComputedStyle {
9 pub properties: HashMap<String, String>,
10}
11
12impl ComputedStyle {
13 pub fn get(&self, prop: &str) -> Option<&str> {
14 self.properties.get(prop).map(|s| s.as_str())
15 }
16
17 pub fn set(&mut self, prop: &str, value: &str) {
18 self.properties.insert(prop.to_string(), value.to_string());
19 }
20}
21
22pub fn compute_styles(
24 sheets: &[StyleSheet],
25 classes: &[String],
26 tag: &str,
27 parent_style: Option<&ComputedStyle>,
28) -> ComputedStyle {
29 let mut map = HashMap::new();
30
31 if let Some(parent) = parent_style {
33 for (prop, val) in &parent.properties {
34 if INHERITED_PROPS.contains(&prop.as_str()) {
35 map.entry(prop.clone()).or_insert_with(|| val.clone());
36 }
37 }
38 }
39
40 for sheet in sheets {
42 for decl in sheet.declarations_for_tag(tag) {
43 map.entry(decl.property.clone()).or_insert(decl.value);
44 }
45 }
46
47 for sheet in sheets {
49 for class in classes {
50 for decl in sheet.declarations_for_class(class) {
51 map.insert(decl.property, decl.value);
52 }
53 }
54 }
55
56 ComputedStyle { properties: map }
57}
58
59const INHERITED_PROPS: &[&str] = &[
61 "color", "font-family", "font-size", "font-style", "font-weight",
62 "line-height", "text-align", "visibility", "cursor",
63 "direction", "letter-spacing", "word-spacing", "white-space",
64];
65
66pub fn parse_px(value: &str) -> f32 {
68 let value = value.trim();
69 if value.ends_with("px") {
70 value[..value.len()-2].trim().parse().unwrap_or(0.0)
71 } else {
72 value.parse().unwrap_or(0.0)
73 }
74}
75
76pub fn parse_color(value: &str) -> (f32, f32, f32, f32) {
79 let value = value.trim().to_lowercase();
80
81 if let Some(rgba) = named_color(&value) { return rgba; }
83
84 if value.starts_with('#') {
86 return parse_hex_color(&value);
87 }
88
89 if value.starts_with("rgb") {
91 let inner = value.trim_start_matches("rgba(").trim_start_matches("rgb(")
92 .trim_end_matches(')');
93 let parts: Vec<f32> = inner.split(',').filter_map(|s| {
94 let trimmed = s.trim();
95 if trimmed.ends_with('%') {
96 trimmed[..trimmed.len()-1].parse::<f32>().ok().map(|v| v / 100.0 * 255.0)
97 } else {
98 trimmed.parse::<f32>().ok()
99 }
100 }).collect();
101 if parts.len() >= 3 {
102 return (parts[0] / 255.0, parts[1] / 255.0, parts[2] / 255.0,
103 parts.get(3).copied().unwrap_or(255.0) / 255.0);
104 }
105 }
106
107 (0.0, 0.0, 0.0, 1.0) }
109
110fn parse_hex_color(hex: &str) -> (f32, f32, f32, f32) {
111 let hex = hex.trim_start_matches('#');
112 let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32 / 255.0;
113 let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
114 let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32 / 255.0;
115 let a = if hex.len() >= 8 {
116 u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f32 / 255.0
117 } else { 1.0 };
118 (r, g, b, a)
119}
120
121fn named_color(name: &str) -> Option<(f32, f32, f32, f32)> {
122 let c = match name {
123 "red" => (1.0, 0.0, 0.0), "green" => (0.0, 0.5, 0.0), "blue" => (0.0, 0.0, 1.0),
124 "white" => (1.0, 1.0, 1.0), "black" => (0.0, 0.0, 0.0),
125 "gray" | "grey" => (0.5, 0.5, 0.5), "silver" => (0.75, 0.75, 0.75),
126 "yellow" => (1.0, 1.0, 0.0), "orange" => (1.0, 0.65, 0.0),
127 "purple" => (0.5, 0.0, 0.5), "pink" => (1.0, 0.75, 0.8),
128 "brown" => (0.65, 0.16, 0.16), "transparent" => (0.0, 0.0, 0.0),
129 _ => return None,
130 };
131 Some((c.0, c.1, c.2, 1.0))
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_parse_px() {
140 assert_eq!(parse_px("16px"), 16.0);
141 assert_eq!(parse_px("0"), 0.0);
142 }
143
144 #[test]
145 fn test_parse_hex_color() {
146 let (r, g, b, a) = parse_color("#ff0000");
147 assert!((r - 1.0).abs() < 0.01);
148 assert!((g - 0.0).abs() < 0.01);
149 assert!((a - 1.0).abs() < 0.01);
150 }
151
152 #[test]
153 fn test_parse_named_color() {
154 let (r, g, b, _) = parse_color("red");
155 assert!((r - 1.0).abs() < 0.01);
156 }
157
158 #[test]
159 fn test_compute_styles() {
160 let css = ".btn { color: red; } .primary { color: blue; font-weight: bold; }";
161 let sheet = crate::parser::parse_css_simple(css);
162 let style = compute_styles(&[sheet], &["btn".into(), "primary".into()], "button", None);
163 assert_eq!(style.get("color").unwrap(), "blue"); assert_eq!(style.get("font-weight").unwrap(), "bold");
165 }
166
167 #[test]
168 fn test_inherited_props() {
169 let parent = {
170 let mut s = ComputedStyle::default();
171 s.set("color", "red");
172 s.set("margin", "10px"); s
174 };
175 let sheet = StyleSheet::default();
176 let child = compute_styles(&[sheet], &[], "span", Some(&parent));
177 assert_eq!(child.get("color").unwrap(), "red"); assert!(child.get("margin").is_none()); }
180}