1use crate::ansi::{Color, Style};
17
18#[derive(Debug, PartialEq, Eq)]
19pub enum ParseError {
20 UnknownAttr(String),
21 UnknownColor(String),
22 BadHex(String),
23 BadToken(String),
24}
25
26impl std::fmt::Display for ParseError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 ParseError::UnknownAttr(t) => write!(f, "unknown attribute `{t}`"),
30 ParseError::UnknownColor(t) => write!(f, "unknown color `{t}`"),
31 ParseError::BadHex(t) => write!(f, "bad hex color `{t}` (expected #RRGGBB)"),
32 ParseError::BadToken(t) => write!(f, "bad token `{t}`"),
33 }
34 }
35}
36
37pub fn parse(spec: &str) -> Result<Style, ParseError> {
40 let mut style = Style::default();
41 let trimmed = spec.trim();
42 if trimmed.is_empty() {
43 return Ok(style);
44 }
45 for raw in trimmed.split(',') {
46 let tok = raw.trim();
47 if tok.is_empty() {
48 continue;
49 }
50 if let Some(c) = tok.strip_prefix("fg=") {
51 style.fg = Some(parse_color(c)?);
52 } else if let Some(c) = tok.strip_prefix("bg=") {
53 style.bg = Some(parse_color(c)?);
54 } else {
55 match tok {
56 "bold" => style.bold = true,
57 "dim" => style.dim = true,
58 "italic" => style.italic = true,
59 "underline" => style.underline = true,
60 "reverse" => style.reverse = true,
61 other if other.contains('=') => {
62 return Err(ParseError::BadToken(other.to_string()))
63 }
64 other => return Err(ParseError::UnknownAttr(other.to_string())),
65 }
66 }
67 }
68 Ok(style)
69}
70
71fn parse_color(s: &str) -> Result<Color, ParseError> {
72 if let Some(hex) = s.strip_prefix('#') {
73 if hex.len() != 6 {
74 return Err(ParseError::BadHex(s.to_string()));
75 }
76 let r = u8::from_str_radix(&hex[0..2], 16)
77 .map_err(|_| ParseError::BadHex(s.to_string()))?;
78 let g = u8::from_str_radix(&hex[2..4], 16)
79 .map_err(|_| ParseError::BadHex(s.to_string()))?;
80 let b = u8::from_str_radix(&hex[4..6], 16)
81 .map_err(|_| ParseError::BadHex(s.to_string()))?;
82 return Ok(Color::Rgb(r, g, b));
83 }
84 if let Ok(n) = s.parse::<u8>() {
85 return Ok(if n < 16 { Color::Ansi(n) } else { Color::Indexed(n) });
86 }
87 let (bright, name) = match s.strip_prefix("bright-") {
88 Some(rest) => (true, rest),
89 None => (false, s),
90 };
91 let base: u8 = match name {
92 "black" => 0,
93 "red" => 1,
94 "green" => 2,
95 "yellow" => 3,
96 "blue" => 4,
97 "magenta" => 5,
98 "cyan" => 6,
99 "white" => 7,
100 _ => return Err(ParseError::UnknownColor(s.to_string())),
101 };
102 Ok(Color::Ansi(if bright { base + 8 } else { base }))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn empty_is_default_style() {
111 assert_eq!(parse("").unwrap(), Style::default());
112 assert_eq!(parse(" ").unwrap(), Style::default());
113 }
114
115 #[test]
116 fn bold_only() {
117 let s = parse("bold").unwrap();
118 assert!(s.bold);
119 assert!(s.fg.is_none());
120 }
121
122 #[test]
123 fn fg_named() {
124 let s = parse("fg=cyan").unwrap();
125 assert_eq!(s.fg, Some(Color::Ansi(6)));
126 }
127
128 #[test]
129 fn fg_bright_named() {
130 let s = parse("fg=bright-red").unwrap();
131 assert_eq!(s.fg, Some(Color::Ansi(9)));
132 }
133
134 #[test]
135 fn bg_hex() {
136 let s = parse("bg=#ff0080").unwrap();
137 assert_eq!(s.bg, Some(Color::Rgb(0xff, 0x00, 0x80)));
138 }
139
140 #[test]
141 fn fg_indexed_under_16_is_ansi() {
142 let s = parse("fg=4").unwrap();
143 assert_eq!(s.fg, Some(Color::Ansi(4)));
144 }
145
146 #[test]
147 fn fg_indexed_over_16_is_palette() {
148 let s = parse("fg=200").unwrap();
149 assert_eq!(s.fg, Some(Color::Indexed(200)));
150 }
151
152 #[test]
153 fn combined_attrs_and_colors() {
154 let s = parse("bold,fg=cyan,bg=black").unwrap();
155 assert!(s.bold);
156 assert_eq!(s.fg, Some(Color::Ansi(6)));
157 assert_eq!(s.bg, Some(Color::Ansi(0)));
158 }
159
160 #[test]
161 fn reverse_attr() {
162 let s = parse("reverse").unwrap();
163 assert!(s.reverse);
164 }
165
166 #[test]
167 fn bad_hex_errors() {
168 assert!(matches!(parse("fg=#12"), Err(ParseError::BadHex(_))));
169 assert!(matches!(parse("fg=#xxxxxx"), Err(ParseError::BadHex(_))));
170 }
171
172 #[test]
173 fn unknown_attr_errors() {
174 assert!(matches!(parse("blink"), Err(ParseError::UnknownAttr(_))));
175 }
176
177 #[test]
178 fn unknown_color_name_errors() {
179 assert!(matches!(parse("fg=puce"), Err(ParseError::UnknownColor(_))));
180 }
181
182 #[test]
183 fn trailing_comma_is_tolerated() {
184 let s = parse("bold,").unwrap();
185 assert!(s.bold);
186 }
187}