1use alloc::string::String;
5
6use super::Color;
7use super::ansi::{ANSI_TABLE, AnsiColor};
8
9const fn ansi_name(color: AnsiColor) -> &'static str {
11 ANSI_TABLE[color as usize].1
12}
13
14fn parse_ansi_name(name: &str) -> Option<AnsiColor> {
17 ANSI_TABLE
18 .iter()
19 .find(|(_, n, _)| n.chars().filter(|c| *c != '-').eq(name.chars()))
20 .map(|(c, _, _)| *c)
21}
22
23fn parse_hex(s: &str) -> Option<(u8, u8, u8)> {
30 let hex = s.strip_prefix('#')?;
31 match hex.len() {
32 3 => {
33 let mut chars = hex.chars();
34 let r = u8::try_from(chars.next()?.to_digit(16)?).ok()?;
35 let g = u8::try_from(chars.next()?.to_digit(16)?).ok()?;
36 let b = u8::try_from(chars.next()?.to_digit(16)?).ok()?;
37 Some((r * 17, g * 17, b * 17))
41 }
42 6 => {
43 if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
44 return None;
45 }
46 let r = u8::from_str_radix(hex.get(0..2)?, 16).ok()?;
47 let g = u8::from_str_radix(hex.get(2..4)?, 16).ok()?;
48 let b = u8::from_str_radix(hex.get(4..6)?, 16).ok()?;
49 Some((r, g, b))
50 }
51 _ => None,
52 }
53}
54
55impl core::fmt::Display for Color {
56 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 match *self {
72 Self::Default => write!(f, "default"),
73 Self::Ansi(color) => write!(f, "{}", ansi_name(color)),
74 Self::Indexed(i) => write!(f, "{i}"),
75 Self::Rgb { r, g, b } => write!(f, "#{r:02x}{g:02x}{b:02x}"),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub struct ParseColorError;
83
84impl core::fmt::Display for ParseColorError {
85 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
86 write!(f, "invalid color string")
87 }
88}
89
90impl core::error::Error for ParseColorError {}
91
92impl core::str::FromStr for Color {
93 type Err = ParseColorError;
94
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
117 let trimmed = s.trim();
118 let normalized: String = trimmed
119 .chars()
120 .filter(|c| !matches!(c, '-' | '_' | ' '))
121 .map(|c| c.to_ascii_lowercase())
122 .collect();
123
124 if normalized == "default" || normalized == "reset" {
125 return Ok(Self::Default);
126 }
127 if let Some(color) = parse_ansi_name(&normalized) {
128 return Ok(Self::Ansi(color));
129 }
130 if normalized.bytes().all(|b| b.is_ascii_digit())
131 && !normalized.is_empty()
132 && let Ok(index) = normalized.parse::<u8>()
133 {
134 return Ok(Self::Indexed(index));
135 }
136 if let Some((r, g, b)) = parse_hex(trimmed) {
137 return Ok(Self::Rgb { r, g, b });
138 }
139 Err(ParseColorError)
140 }
141}
142
143#[cfg(feature = "serde")]
144impl serde::Serialize for Color {
145 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149 where
150 S: serde::Serializer,
151 {
152 use alloc::string::ToString as _;
153
154 serializer.serialize_str(&self.to_string())
155 }
156}
157
158#[cfg(feature = "serde")]
159impl<'de> serde::Deserialize<'de> for Color {
160 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164 where
165 D: serde::Deserializer<'de>,
166 {
167 struct ColorVisitor;
168
169 impl serde::de::Visitor<'_> for ColorVisitor {
170 type Value = Color;
171
172 fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
173 f.write_str(
174 "a color string: \"default\", an ANSI name, a palette index, or #rrggbb hex",
175 )
176 }
177
178 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
179 where
180 E: serde::de::Error,
181 {
182 v.parse().map_err(E::custom)
183 }
184 }
185
186 deserializer.deserialize_str(ColorVisitor)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_display() {
196 use alloc::string::ToString as _;
197
198 assert_eq!(Color::Default.to_string(), "default");
199 assert_eq!(Color::Ansi(AnsiColor::Black).to_string(), "black");
200 assert_eq!(
201 Color::Ansi(AnsiColor::BrightYellow).to_string(),
202 "bright-yellow"
203 );
204 assert_eq!(Color::Indexed(7).to_string(), "7");
205 assert_eq!(Color::rgb(255, 128, 0).to_string(), "#ff8000");
206 }
207
208 #[test]
209 fn test_ansi_name_and_parse_are_inverses() {
210 use super::super::ansi::ANSI_TABLE;
211
212 for (color, _, _) in ANSI_TABLE {
213 let name = ansi_name(color);
214 assert_eq!(
215 name.parse(),
216 Ok(Color::Ansi(color)),
217 "round trip of {name:?}"
218 );
219 }
220 }
221
222 #[test]
223 fn test_from_str_round_trips_display() {
224 use alloc::string::ToString as _;
225
226 let colors = [
227 Color::Default,
228 Color::Ansi(AnsiColor::Black),
229 Color::Ansi(AnsiColor::BrightMagenta),
230 Color::Indexed(200),
231 Color::rgb(18, 52, 86),
232 ];
233 for color in colors {
234 let s = color.to_string();
235 assert_eq!(s.parse(), Ok(color), "round trip of {s:?}");
236 }
237 }
238
239 #[test]
240 fn test_from_str_aliases_and_separators() {
241 assert_eq!("reset".parse(), Ok(Color::Default));
242 assert_eq!("Default".parse(), Ok(Color::Default));
243 assert_eq!("BrightRed".parse(), Ok(Color::Ansi(AnsiColor::BrightRed)));
244 assert_eq!("bright red".parse(), Ok(Color::Ansi(AnsiColor::BrightRed)));
245 assert_eq!("bright_red".parse(), Ok(Color::Ansi(AnsiColor::BrightRed)));
246 assert_eq!("#F80".parse(), Ok(Color::rgb(255, 136, 0)));
247 assert_eq!(" #ff8000 ".parse(), Ok(Color::rgb(255, 128, 0)));
248 }
249
250 #[test]
251 fn test_from_str_invalid() {
252 assert!("not-a-color".parse::<Color>().is_err());
253 assert!("#gg0000".parse::<Color>().is_err());
254 assert!("#12345".parse::<Color>().is_err());
255 assert!("256".parse::<Color>().is_err());
256 }
257
258 #[test]
259 fn from_str_rejects_plus_signed_index_and_hex() {
260 assert!("+5".parse::<Color>().is_err());
263 assert!("#+f0000".parse::<Color>().is_err());
264 }
265
266 #[cfg(feature = "serde")]
267 #[test]
268 fn test_serialize_then_deserialize() -> Result<(), serde_json::Error> {
269 let colors = [
270 Color::Default,
271 Color::Ansi(AnsiColor::BrightGreen),
272 Color::Indexed(42),
273 Color::rgb(255, 0, 255),
274 ];
275 for color in colors {
276 let json = serde_json::to_string(&color)?;
277 assert_eq!(serde_json::from_str::<Color>(&json)?, color);
278 }
279
280 assert_eq!(
281 serde_json::to_string(&Color::rgb(255, 0, 255))?,
282 r##""#ff00ff""##
283 );
284 assert_eq!(
285 serde_json::from_str::<Color>(r#""bright-white""#)?,
286 Color::Ansi(AnsiColor::BrightWhite)
287 );
288 assert!(serde_json::from_str::<Color>(r#""not-a-color""#).is_err());
289
290 Ok(())
291 }
292}