Skip to main content

standout_render/style/
color.rs

1//! Color value parsing for stylesheets.
2//!
3//! Supports multiple color formats:
4//!
5//! - Named colors: `red`, `green`, `blue`, etc. (16 ANSI colors)
6//! - Bright variants: `bright_red`, `bright_green`, etc.
7//! - 256-color palette: `0` through `255`
8//! - RGB hex: `"#ff6b35"` or `"#fff"` (3 or 6 digit)
9//! - RGB tuple: `[255, 107, 53]`
10//!
11//! # Example
12//!
13//! ```rust
14//! use standout::style::ColorDef;
15//!
16//! // Parse from YAML values
17//! let red = ColorDef::parse_value(&serde_yaml::Value::String("red".into())).unwrap();
18//! let hex = ColorDef::parse_value(&serde_yaml::Value::String("#ff6b35".into())).unwrap();
19//! let palette = ColorDef::parse_value(&serde_yaml::Value::Number(208.into())).unwrap();
20//! let rgb = ColorDef::parse_value(&serde_yaml::Value::Sequence(vec![
21//!     serde_yaml::Value::Number(255.into()),
22//!     serde_yaml::Value::Number(107.into()),
23//!     serde_yaml::Value::Number(53.into()),
24//! ])).unwrap();
25//! ```
26
27use console::Color;
28
29/// Parsed color definition from stylesheet.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ColorDef {
32    /// Named ANSI color.
33    Named(Color),
34    /// 256-color palette index.
35    Color256(u8),
36    /// True color RGB.
37    Rgb(u8, u8, u8),
38}
39
40impl ColorDef {
41    /// Parses a color definition from a YAML value.
42    ///
43    /// Supports:
44    /// - Strings: named colors, bright variants, hex codes
45    /// - Numbers: 256-color palette indices
46    /// - Sequences: RGB tuples `[r, g, b]`
47    pub fn parse_value(value: &serde_yaml::Value) -> Result<Self, String> {
48        match value {
49            serde_yaml::Value::String(s) => Self::parse_string(s),
50            serde_yaml::Value::Number(n) => {
51                let index = n
52                    .as_u64()
53                    .ok_or_else(|| format!("Invalid color palette index: {}", n))?;
54                if index > 255 {
55                    return Err(format!(
56                        "Color palette index {} out of range (0-255)",
57                        index
58                    ));
59                }
60                Ok(ColorDef::Color256(index as u8))
61            }
62            serde_yaml::Value::Sequence(seq) => Self::parse_rgb_tuple(seq),
63            _ => Err(format!("Invalid color value: {:?}", value)),
64        }
65    }
66
67    /// Parses a color from a string value.
68    ///
69    /// Supports:
70    /// - Named colors: `red`, `green`, `blue`, etc.
71    /// - Bright variants: `bright_red`, `bright_green`, etc.
72    /// - Hex codes: `#ff6b35` or `#fff`
73    pub fn parse_string(s: &str) -> Result<Self, String> {
74        let s = s.trim();
75
76        // Check for hex color
77        if let Some(hex) = s.strip_prefix('#') {
78            return Self::parse_hex(hex);
79        }
80
81        // Check for named color
82        Self::parse_named(s)
83    }
84
85    /// Parses a hex color code (without the # prefix).
86    fn parse_hex(hex: &str) -> Result<Self, String> {
87        match hex.len() {
88            // 3-digit hex: #rgb -> #rrggbb
89            3 => {
90                let r = u8::from_str_radix(&hex[0..1], 16)
91                    .map_err(|_| format!("Invalid hex: {}", hex))?
92                    * 17;
93                let g = u8::from_str_radix(&hex[1..2], 16)
94                    .map_err(|_| format!("Invalid hex: {}", hex))?
95                    * 17;
96                let b = u8::from_str_radix(&hex[2..3], 16)
97                    .map_err(|_| format!("Invalid hex: {}", hex))?
98                    * 17;
99                Ok(ColorDef::Rgb(r, g, b))
100            }
101            // 6-digit hex: #rrggbb
102            6 => {
103                let r = u8::from_str_radix(&hex[0..2], 16)
104                    .map_err(|_| format!("Invalid hex: {}", hex))?;
105                let g = u8::from_str_radix(&hex[2..4], 16)
106                    .map_err(|_| format!("Invalid hex: {}", hex))?;
107                let b = u8::from_str_radix(&hex[4..6], 16)
108                    .map_err(|_| format!("Invalid hex: {}", hex))?;
109                Ok(ColorDef::Rgb(r, g, b))
110            }
111            _ => Err(format!(
112                "Invalid hex color: #{} (must be 3 or 6 digits)",
113                hex
114            )),
115        }
116    }
117
118    /// Parses a named color (including bright variants).
119    fn parse_named(name: &str) -> Result<Self, String> {
120        let name_lower = name.to_lowercase();
121
122        // Check for bright_ prefix
123        if let Some(base) = name_lower.strip_prefix("bright_") {
124            return Self::parse_bright_color(base);
125        }
126
127        // Standard colors
128        let color = match name_lower.as_str() {
129            "black" => Color::Black,
130            "red" => Color::Red,
131            "green" => Color::Green,
132            "yellow" => Color::Yellow,
133            "blue" => Color::Blue,
134            "magenta" => Color::Magenta,
135            "cyan" => Color::Cyan,
136            "white" => Color::White,
137            // Also accept gray/grey as aliases
138            "gray" | "grey" => Color::White,
139            _ => return Err(format!("Unknown color name: {}", name)),
140        };
141
142        Ok(ColorDef::Named(color))
143    }
144
145    /// Parses a bright color variant.
146    fn parse_bright_color(base: &str) -> Result<Self, String> {
147        // console crate uses Color256 for bright colors (indices 8-15)
148        let index = match base {
149            "black" => 8,
150            "red" => 9,
151            "green" => 10,
152            "yellow" => 11,
153            "blue" => 12,
154            "magenta" => 13,
155            "cyan" => 14,
156            "white" => 15,
157            _ => return Err(format!("Unknown bright color: bright_{}", base)),
158        };
159
160        Ok(ColorDef::Color256(index))
161    }
162
163    /// Parses an RGB tuple from a YAML sequence.
164    fn parse_rgb_tuple(seq: &[serde_yaml::Value]) -> Result<Self, String> {
165        if seq.len() != 3 {
166            return Err(format!(
167                "RGB tuple must have exactly 3 values, got {}",
168                seq.len()
169            ));
170        }
171
172        let mut components = [0u8; 3];
173        for (i, val) in seq.iter().enumerate() {
174            let n = val
175                .as_u64()
176                .ok_or_else(|| format!("RGB component {} is not a number", i))?;
177            if n > 255 {
178                return Err(format!("RGB component {} out of range (0-255): {}", i, n));
179            }
180            components[i] = n as u8;
181        }
182
183        Ok(ColorDef::Rgb(components[0], components[1], components[2]))
184    }
185
186    /// Converts this color definition to a `console::Color`.
187    pub fn to_console_color(&self) -> Color {
188        match self {
189            ColorDef::Named(c) => *c,
190            ColorDef::Color256(n) => Color::Color256(*n),
191            ColorDef::Rgb(r, g, b) => Color::Color256(crate::rgb_to_ansi256((*r, *g, *b))),
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use serde_yaml::Value;
200
201    // =========================================================================
202    // Named color tests
203    // =========================================================================
204
205    #[test]
206    fn test_parse_named_colors() {
207        assert_eq!(
208            ColorDef::parse_string("red").unwrap(),
209            ColorDef::Named(Color::Red)
210        );
211        assert_eq!(
212            ColorDef::parse_string("green").unwrap(),
213            ColorDef::Named(Color::Green)
214        );
215        assert_eq!(
216            ColorDef::parse_string("blue").unwrap(),
217            ColorDef::Named(Color::Blue)
218        );
219        assert_eq!(
220            ColorDef::parse_string("yellow").unwrap(),
221            ColorDef::Named(Color::Yellow)
222        );
223        assert_eq!(
224            ColorDef::parse_string("magenta").unwrap(),
225            ColorDef::Named(Color::Magenta)
226        );
227        assert_eq!(
228            ColorDef::parse_string("cyan").unwrap(),
229            ColorDef::Named(Color::Cyan)
230        );
231        assert_eq!(
232            ColorDef::parse_string("white").unwrap(),
233            ColorDef::Named(Color::White)
234        );
235        assert_eq!(
236            ColorDef::parse_string("black").unwrap(),
237            ColorDef::Named(Color::Black)
238        );
239    }
240
241    #[test]
242    fn test_parse_named_colors_case_insensitive() {
243        assert_eq!(
244            ColorDef::parse_string("RED").unwrap(),
245            ColorDef::Named(Color::Red)
246        );
247        assert_eq!(
248            ColorDef::parse_string("Red").unwrap(),
249            ColorDef::Named(Color::Red)
250        );
251    }
252
253    #[test]
254    fn test_parse_gray_aliases() {
255        assert_eq!(
256            ColorDef::parse_string("gray").unwrap(),
257            ColorDef::Named(Color::White)
258        );
259        assert_eq!(
260            ColorDef::parse_string("grey").unwrap(),
261            ColorDef::Named(Color::White)
262        );
263    }
264
265    #[test]
266    fn test_parse_unknown_color() {
267        assert!(ColorDef::parse_string("purple").is_err());
268        assert!(ColorDef::parse_string("orange").is_err());
269    }
270
271    // =========================================================================
272    // Bright color tests
273    // =========================================================================
274
275    #[test]
276    fn test_parse_bright_colors() {
277        assert_eq!(
278            ColorDef::parse_string("bright_red").unwrap(),
279            ColorDef::Color256(9)
280        );
281        assert_eq!(
282            ColorDef::parse_string("bright_green").unwrap(),
283            ColorDef::Color256(10)
284        );
285        assert_eq!(
286            ColorDef::parse_string("bright_blue").unwrap(),
287            ColorDef::Color256(12)
288        );
289        assert_eq!(
290            ColorDef::parse_string("bright_black").unwrap(),
291            ColorDef::Color256(8)
292        );
293        assert_eq!(
294            ColorDef::parse_string("bright_white").unwrap(),
295            ColorDef::Color256(15)
296        );
297    }
298
299    #[test]
300    fn test_parse_unknown_bright_color() {
301        assert!(ColorDef::parse_string("bright_purple").is_err());
302    }
303
304    // =========================================================================
305    // Hex color tests
306    // =========================================================================
307
308    #[test]
309    fn test_parse_hex_6_digit() {
310        assert_eq!(
311            ColorDef::parse_string("#ff6b35").unwrap(),
312            ColorDef::Rgb(255, 107, 53)
313        );
314        assert_eq!(
315            ColorDef::parse_string("#000000").unwrap(),
316            ColorDef::Rgb(0, 0, 0)
317        );
318        assert_eq!(
319            ColorDef::parse_string("#ffffff").unwrap(),
320            ColorDef::Rgb(255, 255, 255)
321        );
322    }
323
324    #[test]
325    fn test_parse_hex_3_digit() {
326        assert_eq!(
327            ColorDef::parse_string("#fff").unwrap(),
328            ColorDef::Rgb(255, 255, 255)
329        );
330        assert_eq!(
331            ColorDef::parse_string("#000").unwrap(),
332            ColorDef::Rgb(0, 0, 0)
333        );
334        assert_eq!(
335            ColorDef::parse_string("#f80").unwrap(),
336            ColorDef::Rgb(255, 136, 0)
337        );
338    }
339
340    #[test]
341    fn test_parse_hex_case_insensitive() {
342        assert_eq!(
343            ColorDef::parse_string("#FF6B35").unwrap(),
344            ColorDef::Rgb(255, 107, 53)
345        );
346        assert_eq!(
347            ColorDef::parse_string("#FFF").unwrap(),
348            ColorDef::Rgb(255, 255, 255)
349        );
350    }
351
352    #[test]
353    fn test_parse_hex_invalid() {
354        assert!(ColorDef::parse_string("#ff").is_err());
355        assert!(ColorDef::parse_string("#ffff").is_err());
356        assert!(ColorDef::parse_string("#gggggg").is_err());
357    }
358
359    // =========================================================================
360    // YAML value tests
361    // =========================================================================
362
363    #[test]
364    fn test_parse_value_string() {
365        let val = Value::String("red".into());
366        assert_eq!(
367            ColorDef::parse_value(&val).unwrap(),
368            ColorDef::Named(Color::Red)
369        );
370    }
371
372    #[test]
373    fn test_parse_value_number() {
374        let val = Value::Number(208.into());
375        assert_eq!(
376            ColorDef::parse_value(&val).unwrap(),
377            ColorDef::Color256(208)
378        );
379    }
380
381    #[test]
382    fn test_parse_value_number_out_of_range() {
383        let val = Value::Number(256.into());
384        assert!(ColorDef::parse_value(&val).is_err());
385    }
386
387    #[test]
388    fn test_parse_value_sequence() {
389        let val = Value::Sequence(vec![
390            Value::Number(255.into()),
391            Value::Number(107.into()),
392            Value::Number(53.into()),
393        ]);
394        assert_eq!(
395            ColorDef::parse_value(&val).unwrap(),
396            ColorDef::Rgb(255, 107, 53)
397        );
398    }
399
400    #[test]
401    fn test_parse_value_sequence_wrong_length() {
402        let val = Value::Sequence(vec![Value::Number(255.into()), Value::Number(107.into())]);
403        assert!(ColorDef::parse_value(&val).is_err());
404    }
405
406    #[test]
407    fn test_parse_value_sequence_out_of_range() {
408        let val = Value::Sequence(vec![
409            Value::Number(256.into()),
410            Value::Number(107.into()),
411            Value::Number(53.into()),
412        ]);
413        assert!(ColorDef::parse_value(&val).is_err());
414    }
415
416    // =========================================================================
417    // to_console_color tests
418    // =========================================================================
419
420    #[test]
421    fn test_to_console_color_named() {
422        let c = ColorDef::Named(Color::Red);
423        assert_eq!(c.to_console_color(), Color::Red);
424    }
425
426    #[test]
427    fn test_to_console_color_256() {
428        let c = ColorDef::Color256(208);
429        assert_eq!(c.to_console_color(), Color::Color256(208));
430    }
431
432    #[test]
433    fn test_to_console_color_rgb() {
434        let c = ColorDef::Rgb(255, 107, 53);
435        // RGB gets converted to 256 color via rgb_to_ansi256
436        if let Color::Color256(_) = c.to_console_color() {
437            // OK - it converted
438        } else {
439            panic!("Expected Color256");
440        }
441    }
442}