dampen_core/parser/
gradient.rs

1//! Gradient parsing utilities
2//!
3//! This module provides parsers for CSS-style gradient strings.
4
5use crate::ir::style::{Color, ColorStop, Gradient, RadialShape};
6
7/// Parse a linear or radial gradient from a string
8///
9/// # Formats
10/// - `linear-gradient(<angle>, <color-stop>, <color-stop>, ...)`
11/// - `radial-gradient(<shape>, <color-stop>, <color-stop>, ...)`
12///
13/// # Examples
14/// ```rust
15/// use dampen_core::parser::gradient::parse_gradient;
16///
17/// let grad = parse_gradient("linear-gradient(90deg, red, blue)").unwrap();
18/// ```
19pub fn parse_gradient(s: &str) -> Result<Gradient, String> {
20    let s = s.trim();
21
22    if s.starts_with("linear-gradient(") && s.ends_with(')') {
23        parse_linear_gradient(s)
24    } else if s.starts_with("radial-gradient(") && s.ends_with(')') {
25        parse_radial_gradient(s)
26    } else {
27        Err(format!(
28            "Invalid gradient format: '{}'. Expected linear-gradient(...) or radial-gradient(...)",
29            s
30        ))
31    }
32}
33
34/// Parse linear gradient: linear-gradient(<angle>, <color-stop>, ...)
35fn parse_linear_gradient(s: &str) -> Result<Gradient, String> {
36    let inner = &s[16..s.len() - 1]; // Remove "linear-gradient(" and ")"
37    let parts: Vec<&str> = inner.split(',').collect();
38
39    if parts.len() < 2 {
40        return Err("Linear gradient requires at least angle and one color stop".to_string());
41    }
42
43    let angle = parse_angle(parts[0])?;
44    let stops = parse_color_stops(&parts[1..])?;
45
46    Ok(Gradient::Linear { angle, stops })
47}
48
49/// Parse radial gradient: radial-gradient(<shape>, <color-stop>, ...)
50fn parse_radial_gradient(s: &str) -> Result<Gradient, String> {
51    let inner = &s[16..s.len() - 1]; // Remove "radial-gradient(" and ")"
52    let parts: Vec<&str> = inner.split(',').collect();
53
54    if parts.len() < 2 {
55        return Err("Radial gradient requires at least shape and one color stop".to_string());
56    }
57
58    let shape = parse_shape(parts[0])?;
59    let stops = parse_color_stops(&parts[1..])?;
60
61    Ok(Gradient::Radial { shape, stops })
62}
63
64/// Parse angle: "90deg", "1.5rad", "0.25turn"
65pub fn parse_angle(s: &str) -> Result<f32, String> {
66    let s = s.trim();
67
68    if let Some(num) = s.strip_suffix("deg") {
69        let value: f32 = num
70            .parse()
71            .map_err(|_| format!("Invalid degree value: {}", s))?;
72        Ok(value % 360.0)
73    } else if let Some(num) = s.strip_suffix("rad") {
74        let value: f32 = num
75            .parse()
76            .map_err(|_| format!("Invalid radian value: {}", s))?;
77        Ok(value * 180.0 / std::f32::consts::PI)
78    } else if let Some(num) = s.strip_suffix("turn") {
79        let value: f32 = num
80            .parse()
81            .map_err(|_| format!("Invalid turn value: {}", s))?;
82        Ok(value * 360.0)
83    } else {
84        // Try to parse as plain number (degrees)
85        let value: f32 = s.parse().map_err(|_| format!("Invalid angle: {}", s))?;
86        Ok(value)
87    }
88}
89
90/// Parse radial shape: "circle" or "ellipse"
91fn parse_shape(s: &str) -> Result<RadialShape, String> {
92    match s.trim().to_lowercase().as_str() {
93        "circle" => Ok(RadialShape::Circle),
94        "ellipse" => Ok(RadialShape::Ellipse),
95        _ => Err(format!(
96            "Invalid radial shape: '{}'. Expected circle or ellipse",
97            s
98        )),
99    }
100}
101
102/// Parse color stops: "red", "red 0%", "rgb(255,0,0) 50%"
103pub fn parse_color_stops(parts: &[&str]) -> Result<Vec<ColorStop>, String> {
104    let mut stops = Vec::new();
105
106    for part in parts {
107        let part = part.trim();
108        let stop = parse_color_stop(part)?;
109        stops.push(stop);
110    }
111
112    Ok(stops)
113}
114
115/// Parse a single color stop
116pub fn parse_color_stop(s: &str) -> Result<ColorStop, String> {
117    // Split by whitespace to separate color and optional offset
118    let parts: Vec<&str> = s.split_whitespace().collect();
119
120    if parts.is_empty() {
121        return Err("Empty color stop".to_string());
122    }
123
124    let color_str = parts[0];
125    let color = Color::parse(color_str)?;
126
127    // Optional offset
128    let offset = if parts.len() > 1 {
129        let offset_str = parts[1];
130        if let Some(num) = offset_str.strip_suffix('%') {
131            let value: f32 = num
132                .parse()
133                .map_err(|_| format!("Invalid offset: {}", offset_str))?;
134            value / 100.0
135        } else {
136            let value: f32 = offset_str
137                .parse()
138                .map_err(|_| format!("Invalid offset: {}", offset_str))?;
139            value
140        }
141    } else {
142        // No offset specified, will be determined by position
143        // For now, we'll use 0.0 and caller should normalize
144        0.0
145    };
146
147    Ok(ColorStop { color, offset })
148}