anstyle_git/
lib.rs

1//! `anstyle_git::parse` parses a color configuration string (in Git syntax)
2//! into an `anstyle::Style`:
3//!
4//! # Examples
5//!
6//! ```rust
7//! let style = anstyle_git::parse("bold red blue").unwrap();
8//! assert_eq!(style, anstyle::AnsiColor::Red.on(anstyle::AnsiColor::Blue) | anstyle::Effects::BOLD);
9//!
10//! let hyperlink_style = anstyle_git::parse("#0000ee ul").unwrap();
11//! assert_eq!(hyperlink_style, anstyle::RgbColor(0x00, 0x00, 0xee).on_default() | anstyle::Effects::UNDERLINE);
12//! ```
13
14#![cfg_attr(docsrs, feature(doc_auto_cfg))]
15#![warn(missing_docs)]
16#![warn(clippy::print_stderr)]
17#![warn(clippy::print_stdout)]
18
19/// Parse a string in Git's color configuration syntax into an
20/// [`anstyle::Style`].
21pub fn parse(s: &str) -> Result<anstyle::Style, Error> {
22    let mut style = anstyle::Style::new();
23    let mut num_colors = 0;
24    let mut effects = anstyle::Effects::new();
25    for word in s.split_whitespace() {
26        match word.to_lowercase().as_ref() {
27            "nobold" | "no-bold" => {
28                effects = effects.remove(anstyle::Effects::BOLD);
29            }
30            "bold" => {
31                effects = effects.insert(anstyle::Effects::BOLD);
32            }
33            "nodim" | "no-dim" => {
34                effects = effects.remove(anstyle::Effects::DIMMED);
35            }
36            "dim" => {
37                effects = effects.insert(anstyle::Effects::DIMMED);
38            }
39            "noul" | "no-ul" => {
40                effects = effects.remove(anstyle::Effects::UNDERLINE);
41            }
42            "ul" => {
43                effects = effects.insert(anstyle::Effects::UNDERLINE);
44            }
45            "noblink" | "no-blink" => {
46                effects = effects.remove(anstyle::Effects::BLINK);
47            }
48            "blink" => {
49                effects = effects.insert(anstyle::Effects::BLINK);
50            }
51            "noreverse" | "no-reverse" => {
52                effects = effects.remove(anstyle::Effects::INVERT);
53            }
54            "reverse" => {
55                effects = effects.insert(anstyle::Effects::INVERT);
56            }
57            "noitalic" | "no-italic" => {
58                effects = effects.remove(anstyle::Effects::ITALIC);
59            }
60            "italic" => {
61                effects = effects.insert(anstyle::Effects::ITALIC);
62            }
63            "nostrike" | "no-strike" => {
64                effects = effects.remove(anstyle::Effects::STRIKETHROUGH);
65            }
66            "strike" => {
67                effects = effects.insert(anstyle::Effects::STRIKETHROUGH);
68            }
69            w => {
70                if let Ok(color) = parse_color(w) {
71                    match num_colors {
72                        0 => {
73                            style = style.fg_color(color);
74                            num_colors += 1;
75                        }
76                        1 => {
77                            style = style.bg_color(color);
78                            num_colors += 1;
79                        }
80                        _ => {
81                            return Err(Error::ExtraColor {
82                                style: s.to_owned(),
83                                word: word.to_owned(),
84                            });
85                        }
86                    }
87                } else {
88                    return Err(Error::UnknownWord {
89                        style: s.to_owned(),
90                        word: word.to_owned(),
91                    });
92                }
93            }
94        }
95    }
96    style |= effects;
97    Ok(style)
98}
99
100fn parse_color(word: &str) -> Result<Option<anstyle::Color>, ()> {
101    let color = match word {
102        "normal" => None,
103        "-1" => None,
104        "black" => Some(anstyle::AnsiColor::Black.into()),
105        "red" => Some(anstyle::AnsiColor::Red.into()),
106        "green" => Some(anstyle::AnsiColor::Green.into()),
107        "yellow" => Some(anstyle::AnsiColor::Yellow.into()),
108        "blue" => Some(anstyle::AnsiColor::Blue.into()),
109        "magenta" => Some(anstyle::AnsiColor::Magenta.into()),
110        "cyan" => Some(anstyle::AnsiColor::Cyan.into()),
111        "white" => Some(anstyle::AnsiColor::White.into()),
112        _ => {
113            if let Some(hex) = word.strip_prefix('#') {
114                let l = hex.len();
115                if l != 3 && l != 6 {
116                    return Err(());
117                }
118                let l = l / 3;
119                if let (Ok(r), Ok(g), Ok(b)) = (
120                    u8::from_str_radix(&hex[0..l], 16),
121                    u8::from_str_radix(&hex[l..(2 * l)], 16),
122                    u8::from_str_radix(&hex[(2 * l)..(3 * l)], 16),
123                ) {
124                    Some(anstyle::Color::from((r, g, b)))
125                } else {
126                    return Err(());
127                }
128            } else if let Ok(n) = word.parse::<u8>() {
129                Some(anstyle::Color::from(n))
130            } else {
131                return Err(());
132            }
133        }
134    };
135    Ok(color)
136}
137
138/// Type for errors returned by the parser.
139#[derive(Debug, PartialEq, Eq)]
140#[non_exhaustive]
141pub enum Error {
142    /// An extra color appeared after the foreground and background colors.
143    ExtraColor {
144        /// Original style
145        style: String,
146        /// Extra color
147        word: String,
148    },
149    /// An unknown word appeared.
150    UnknownWord {
151        /// Original style
152        style: String,
153        /// Unknown word
154        word: String,
155    },
156}
157
158impl std::fmt::Display for Error {
159    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        match self {
161            Self::ExtraColor { style, word } => {
162                write!(
163                    fmt,
164                    "Error parsing style \"{style}\": extra color \"{word}\""
165                )
166            }
167            Self::UnknownWord { style, word } => {
168                write!(
169                    fmt,
170                    "Error parsing style \"{style}\": unknown word: \"{word}\""
171                )
172            }
173        }
174    }
175}
176
177impl std::error::Error for Error {}
178
179#[cfg(test)]
180mod tests {
181    use super::Error::*;
182    use super::*;
183
184    use anstyle::AnsiColor::*;
185    use anstyle::*;
186
187    #[test]
188    fn test_parse_style() {
189        macro_rules! test {
190            ($s:expr => $style:expr) => {
191                assert_eq!(parse($s).unwrap(), $style);
192            };
193        }
194
195        test!("" => Style::new());
196        test!("  " => Style::new());
197        test!("normal" => Style::new());
198        test!("normal normal" => Style::new());
199        test!("-1 normal" => Style::new());
200        test!("red" => Red.on_default());
201        test!("red blue" => Red.on(Blue));
202        test!("   red blue   " => Red.on(Blue));
203        test!("red\tblue" => Red.on(Blue));
204        test!("red\n blue" => Red.on(Blue));
205        test!("red\r\n blue" => Red.on(Blue));
206        test!("blue red" => Blue.on(Red));
207        test!("yellow green" => Yellow.on(Green));
208        test!("white magenta" => White.on(Magenta));
209        test!("black cyan" => Black.on(Cyan));
210        test!("red normal" => Red.on_default());
211        test!("normal red" => Style::new().bg_color(Some(Red.into())));
212        test!("0" => Ansi256Color(0).on_default());
213        test!("8 3" => Ansi256Color(8).on(Ansi256Color(3)));
214        test!("255" => Ansi256Color(255).on_default());
215        test!("255 -1" => Ansi256Color(255).on_default());
216        test!("#000000" => RgbColor(0,0,0).on_default());
217        test!("#204060" => RgbColor(0x20,0x40,0x60).on_default());
218        test!("#1a2b3c" => RgbColor(0x1a,0x2b,0x3c).on_default());
219        test!("#000" => RgbColor(0,0,0).on_default());
220        test!("#cba" => RgbColor(0xc,0xb,0xa).on_default());
221        test!("#cba   " => RgbColor(0xc,0xb,0xa).on_default());
222        test!("#987 #135" => RgbColor(9,8,7).on(RgbColor(1, 3, 5)));
223        test!("#987    #135   " => RgbColor(9,8,7).on(RgbColor(1, 3, 5)));
224        test!("#123 #abcdef" => RgbColor(1,2,3).on(RgbColor(0xab, 0xcd, 0xef)));
225        test!("#654321 #a9b" => RgbColor(0x65,0x43,0x21).on(RgbColor(0xa, 0x9, 0xb)));
226
227        test!("bold cyan white" => Cyan.on(White).bold());
228        test!("bold cyan nobold white" => Cyan.on(White));
229        test!("bold cyan reverse white nobold" => Cyan.on(White).invert());
230        test!("bold cyan ul white dim" => Cyan.on(White).bold().underline().dimmed());
231        test!("ul cyan white no-ul" => Cyan.on(White));
232        test!("italic cyan white" => Cyan.on(White).italic());
233        test!("strike cyan white" => Cyan.on(White).strikethrough());
234        test!("blink #050505 white" => RgbColor(5,5,5).on(White).blink());
235        test!("bold #987 green" => RgbColor(9,8,7).on(Green).bold());
236        test!("strike #147 #cba" => RgbColor(1,4,7).on(RgbColor(0xc, 0xb, 0xa)).strikethrough());
237    }
238
239    #[test]
240    fn test_parse_style_err() {
241        macro_rules! test {
242            ($s:expr => $err:ident $word:expr) => {
243                assert_eq!(
244                    parse($s),
245                    Err($err {
246                        style: $s.to_owned(),
247                        word: $word.to_owned()
248                    })
249                );
250            };
251        }
252
253        test!("red blue green" => ExtraColor "green");
254        test!("red blue 123" => ExtraColor "123");
255        test!("123 red blue" => ExtraColor "blue");
256        test!("red blue normal" => ExtraColor "normal");
257        test!("red blue -1" => ExtraColor "-1");
258        test!("yellow green #abcdef" => ExtraColor "#abcdef");
259        test!("#123456 #654321 #abcdef" => ExtraColor "#abcdef");
260        test!("#123 #654 #abc" => ExtraColor "#abc");
261        test!("#123 #654 #abcdef" => ExtraColor "#abcdef");
262        test!("#123456 #654321 #abc" => ExtraColor "#abc");
263        test!("bold red blue green" => ExtraColor "green");
264        test!("red bold blue green" => ExtraColor "green");
265        test!("red blue bold green" => ExtraColor "green");
266        test!("red blue green bold" => ExtraColor "green");
267
268        test!("256" => UnknownWord "256");
269        test!("-2" => UnknownWord "-2");
270        test!("-" => UnknownWord "-");
271        test!("- 1" => UnknownWord "-");
272        test!("123-1" => UnknownWord "123-1");
273        test!("blue1" => UnknownWord "blue1");
274        test!("blue-1" => UnknownWord "blue-1");
275        test!("no" => UnknownWord "no");
276        test!("nou" => UnknownWord "nou");
277        test!("noblue" => UnknownWord "noblue");
278        test!("no#123456" => UnknownWord "no#123456");
279        test!("no-" => UnknownWord "no-");
280        test!("no-u" => UnknownWord "no-u");
281        test!("no-green" => UnknownWord "no-green");
282        test!("no-#123456" => UnknownWord "no-#123456");
283        test!("#" => UnknownWord "#");
284        test!("#1" => UnknownWord "#1");
285        test!("#12" => UnknownWord "#12");
286        test!("#1234" => UnknownWord "#1234");
287        test!("#12345" => UnknownWord "#12345");
288        test!("#1234567" => UnknownWord "#1234567");
289        test!("#12345678" => UnknownWord "#12345678");
290        test!("#123456789" => UnknownWord "#123456789");
291        test!("#123456789abc" => UnknownWord "#123456789abc");
292        test!("#bcdefg" => UnknownWord "#bcdefg");
293        test!("#blue" => UnknownWord "#blue");
294        test!("blue#123456" => UnknownWord "blue#123456");
295    }
296}