1#![cfg_attr(docsrs, feature(doc_cfg))]
15#![warn(missing_docs)]
16#![warn(clippy::print_stderr)]
17#![warn(clippy::print_stdout)]
18
19pub 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 Some(parse_hex_color(hex)?)
115 } else if let Ok(n) = word.parse::<u8>() {
116 Some(anstyle::Color::from(n))
117 } else {
118 return Err(());
119 }
120 }
121 };
122 Ok(color)
123}
124
125fn parse_hex_color(hex: &str) -> Result<anstyle::Color, ()> {
126 let l = hex.len();
127 if l != 3 && l != 6 {
128 return Err(());
129 }
130 let l = l / 3;
131 if let (Ok(r), Ok(g), Ok(b)) = (
132 u8::from_str_radix(&hex[0..l], 16),
133 u8::from_str_radix(&hex[l..(2 * l)], 16),
134 u8::from_str_radix(&hex[(2 * l)..(3 * l)], 16),
135 ) {
136 let m = if l == 1 { 0x11 } else { 1 };
137 Ok(anstyle::Color::from((r * m, g * m, b * m)))
138 } else {
139 Err(())
140 }
141}
142
143#[derive(Debug, PartialEq, Eq)]
145#[non_exhaustive]
146pub enum Error {
147 ExtraColor {
149 style: String,
151 word: String,
153 },
154 UnknownWord {
156 style: String,
158 word: String,
160 },
161}
162
163impl std::fmt::Display for Error {
164 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 Self::ExtraColor { style, word } => {
167 write!(
168 fmt,
169 "Error parsing style \"{style}\": extra color \"{word}\""
170 )
171 }
172 Self::UnknownWord { style, word } => {
173 write!(
174 fmt,
175 "Error parsing style \"{style}\": unknown word: \"{word}\""
176 )
177 }
178 }
179 }
180}
181
182impl std::error::Error for Error {}
183
184#[cfg(test)]
185mod tests {
186 use super::Error::*;
187 use super::*;
188
189 use anstyle::AnsiColor::*;
190 use anstyle::*;
191
192 #[test]
193 fn test_parse_style() {
194 macro_rules! test {
195 ($s:expr => $style:expr) => {
196 assert_eq!(parse($s).unwrap(), $style);
197 };
198 }
199
200 test!("" => Style::new());
201 test!(" " => Style::new());
202 test!("normal" => Style::new());
203 test!("normal normal" => Style::new());
204 test!("-1 normal" => Style::new());
205 test!("red" => Red.on_default());
206 test!("red blue" => Red.on(Blue));
207 test!(" red blue " => Red.on(Blue));
208 test!("red\tblue" => Red.on(Blue));
209 test!("red\n blue" => Red.on(Blue));
210 test!("red\r\n blue" => Red.on(Blue));
211 test!("blue red" => Blue.on(Red));
212 test!("yellow green" => Yellow.on(Green));
213 test!("white magenta" => White.on(Magenta));
214 test!("black cyan" => Black.on(Cyan));
215 test!("red normal" => Red.on_default());
216 test!("normal red" => Style::new().bg_color(Some(Red.into())));
217 test!("0" => Ansi256Color(0).on_default());
218 test!("8 3" => Ansi256Color(8).on(Ansi256Color(3)));
219 test!("255" => Ansi256Color(255).on_default());
220 test!("255 -1" => Ansi256Color(255).on_default());
221 test!("#000000" => RgbColor(0,0,0).on_default());
222 test!("#204060" => RgbColor(0x20,0x40,0x60).on_default());
223 test!("#1a2b3c" => RgbColor(0x1a,0x2b,0x3c).on_default());
224 test!("#000" => RgbColor(0,0,0).on_default());
225 test!("#cba" => RgbColor(0xcc, 0xbb, 0xaa).on_default());
226 test!("#cba " => RgbColor(0xcc, 0xbb, 0xaa).on_default());
227 test!("#987 #135" => RgbColor(0x99, 0x88, 0x77).on(RgbColor(0x11, 0x33, 0x55)));
228 test!("#987 #135 " => RgbColor(0x99, 0x88, 0x77).on(RgbColor(0x11, 0x33, 0x55)));
229 test!("#123 #abcdef" => RgbColor(0x11, 0x22, 0x33).on(RgbColor(0xab, 0xcd, 0xef)));
230 test!("#654321 #a9b" => RgbColor(0x65, 0x43, 0x21).on(RgbColor(0xaa, 0x99, 0xbb)));
231
232 test!("bold cyan white" => Cyan.on(White).bold());
233 test!("bold cyan nobold white" => Cyan.on(White));
234 test!("bold cyan reverse white nobold" => Cyan.on(White).invert());
235 test!("bold cyan ul white dim" => Cyan.on(White).bold().underline().dimmed());
236 test!("ul cyan white no-ul" => Cyan.on(White));
237 test!("italic cyan white" => Cyan.on(White).italic());
238 test!("strike cyan white" => Cyan.on(White).strikethrough());
239 test!("blink #050505 white" => RgbColor(5,5,5).on(White).blink());
240 test!("bold #987 green" => RgbColor(0x99, 0x88, 0x77).on(Green).bold());
241 test!("strike #147 #cba" => RgbColor(0x11, 0x44, 0x77).on(RgbColor(0xcc, 0xbb, 0xaa)).strikethrough());
242 }
243
244 #[test]
245 fn test_parse_style_err() {
246 macro_rules! test {
247 ($s:expr => $err:ident $word:expr) => {
248 assert_eq!(
249 parse($s),
250 Err($err {
251 style: $s.to_owned(),
252 word: $word.to_owned()
253 })
254 );
255 };
256 }
257
258 test!("red blue green" => ExtraColor "green");
259 test!("red blue 123" => ExtraColor "123");
260 test!("123 red blue" => ExtraColor "blue");
261 test!("red blue normal" => ExtraColor "normal");
262 test!("red blue -1" => ExtraColor "-1");
263 test!("yellow green #abcdef" => ExtraColor "#abcdef");
264 test!("#123456 #654321 #abcdef" => ExtraColor "#abcdef");
265 test!("#123 #654 #abc" => ExtraColor "#abc");
266 test!("#123 #654 #abcdef" => ExtraColor "#abcdef");
267 test!("#123456 #654321 #abc" => ExtraColor "#abc");
268 test!("bold red blue green" => ExtraColor "green");
269 test!("red bold blue green" => ExtraColor "green");
270 test!("red blue bold green" => ExtraColor "green");
271 test!("red blue green bold" => ExtraColor "green");
272
273 test!("256" => UnknownWord "256");
274 test!("-2" => UnknownWord "-2");
275 test!("-" => UnknownWord "-");
276 test!("- 1" => UnknownWord "-");
277 test!("123-1" => UnknownWord "123-1");
278 test!("blue1" => UnknownWord "blue1");
279 test!("blue-1" => UnknownWord "blue-1");
280 test!("no" => UnknownWord "no");
281 test!("nou" => UnknownWord "nou");
282 test!("noblue" => UnknownWord "noblue");
283 test!("no#123456" => UnknownWord "no#123456");
284 test!("no-" => UnknownWord "no-");
285 test!("no-u" => UnknownWord "no-u");
286 test!("no-green" => UnknownWord "no-green");
287 test!("no-#123456" => UnknownWord "no-#123456");
288 test!("#" => UnknownWord "#");
289 test!("#1" => UnknownWord "#1");
290 test!("#12" => UnknownWord "#12");
291 test!("#1234" => UnknownWord "#1234");
292 test!("#12345" => UnknownWord "#12345");
293 test!("#1234567" => UnknownWord "#1234567");
294 test!("#12345678" => UnknownWord "#12345678");
295 test!("#123456789" => UnknownWord "#123456789");
296 test!("#123456789abc" => UnknownWord "#123456789abc");
297 test!("#bcdefg" => UnknownWord "#bcdefg");
298 test!("#blue" => UnknownWord "#blue");
299 test!("blue#123456" => UnknownWord "blue#123456");
300 }
301}
302
303#[doc = include_str!("../README.md")]
304#[cfg(doctest)]
305pub struct ReadmeDoctests;