1use std::fmt::Write;
2
3use crate::lexer::EmphasisType;
4
5#[derive(Debug, PartialEq)]
7pub enum Ground {
8 Foreground,
10 Background,
12}
13
14#[derive(Default)]
16pub struct Style {
17 pub fg: Option<Color>,
19 pub bg: Option<Color>,
21 pub bold: bool,
23 pub dim: bool,
25 pub italic: bool,
27 pub underline: bool,
29 pub strikethrough: bool,
31 pub blink: bool,
33}
34
35#[derive(Debug, PartialEq)]
37pub enum NamedColor {
38 Black,
39 Red,
40 Green,
41 Yellow,
42 Blue,
43 Magenta,
44 Cyan,
45 White,
46}
47
48#[derive(Debug, PartialEq)]
50pub enum Color {
51 Named(NamedColor),
52 Ansi256(u8),
53 Rgb(u8, u8, u8),
54}
55
56impl NamedColor {
57 pub(crate) fn from_str(input: &str) -> Option<Self> {
62 match input {
63 "black" => Some(Self::Black),
64 "red" => Some(Self::Red),
65 "green" => Some(Self::Green),
66 "yellow" => Some(Self::Yellow),
67 "blue" => Some(Self::Blue),
68 "magenta" => Some(Self::Magenta),
69 "cyan" => Some(Self::Cyan),
70 "white" => Some(Self::White),
71 _ => None,
72 }
73 }
74}
75
76fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
80 let mut seq = String::from("\x1b[");
81
82 for (i, n) in vec.iter().enumerate() {
83 if i != 0 {
84 seq.push(';');
85 }
86 write!(seq, "{n}").unwrap();
87 }
88
89 seq.push('m');
90 seq
91}
92
93fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
96 let addend: u8 = match param {
97 Ground::Background => 10,
98 Ground::Foreground => 0,
99 };
100 match color {
101 Color::Named(named) => {
102 ansi.push(match named {
103 NamedColor::Black => 30 + addend,
104 NamedColor::Red => 31 + addend,
105 NamedColor::Green => 32 + addend,
106 NamedColor::Yellow => 33 + addend,
107 NamedColor::Blue => 34 + addend,
108 NamedColor::Magenta => 35 + addend,
109 NamedColor::Cyan => 36 + addend,
110 NamedColor::White => 37 + addend,
111 });
112 }
113 Color::Ansi256(v) => {
114 ansi.extend_from_slice(&[38 + addend, 5, *v]);
115 }
116 Color::Rgb(r, g, b) => {
117 ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
118 }
119 }
120}
121
122pub(crate) fn color_to_ansi(color: &Color, ground: Ground) -> String {
130 let mut ansi: Vec<u8> = Vec::new();
131 encode_color_sgr(&mut ansi, ground, color);
132
133 vec_to_ansi_seq(ansi)
134}
135
136pub(crate) fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
138 let code = match emphasis {
139 EmphasisType::Bold => 1,
140 EmphasisType::Dim => 2,
141 EmphasisType::Italic => 3,
142 EmphasisType::Underline => 4,
143 EmphasisType::Blink => 5,
144 EmphasisType::Strikethrough => 9,
145 };
146 vec_to_ansi_seq(vec![code])
147}
148
149pub(crate) fn style_to_ansi(style: &Style) -> String {
154 let mut ansi: Vec<u8> = Vec::new();
155
156 for (enabled, code) in [
157 (style.bold, 1),
158 (style.dim, 2),
159 (style.italic, 3),
160 (style.underline, 4),
161 (style.blink, 5),
162 (style.strikethrough, 9),
163 ] {
164 if enabled {
165 ansi.push(code);
166 }
167 }
168
169 if let Some(fg) = &style.fg {
170 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
171 }
172 if let Some(bg) = &style.bg {
173 encode_color_sgr(&mut ansi, Ground::Background, bg);
174 }
175
176 if ansi.is_empty() {
177 return String::new();
178 }
179
180 vec_to_ansi_seq(ansi)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::lexer::EmphasisType;
187
188 #[test]
191 fn test_named_color_from_str_known_colors() {
192 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
193 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
194 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
195 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
196 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
197 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
198 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
199 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
200 }
201
202 #[test]
203 fn test_named_color_from_str_unknown_returns_none() {
204 assert_eq!(NamedColor::from_str("purple"), None);
205 }
206
207 #[test]
208 fn test_named_color_from_str_case_sensitive() {
209 assert_eq!(NamedColor::from_str("Red"), None);
210 assert_eq!(NamedColor::from_str("RED"), None);
211 }
212
213 #[test]
214 fn test_named_color_from_str_empty_returns_none() {
215 assert_eq!(NamedColor::from_str(""), None);
216 }
217
218 #[test]
221 fn test_vec_to_ansi_seq_single_param() {
222 let result = vec_to_ansi_seq(vec![1]);
223 assert_eq!(result, "\x1b[1m");
224 }
225
226 #[test]
227 fn test_vec_to_ansi_seq_multiple_params() {
228 let result = vec_to_ansi_seq(vec![1, 31]);
229 assert_eq!(result, "\x1b[1;31m");
230 }
231
232 #[test]
233 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
234 let result = vec_to_ansi_seq(vec![]);
235 assert_eq!(result, "\x1b[m");
236 }
237
238 #[test]
241 fn test_color_to_ansi_named_foreground() {
242 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
243 assert_eq!(result, "\x1b[31m");
244 }
245
246 #[test]
247 fn test_color_to_ansi_named_background() {
248 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
249 assert_eq!(result, "\x1b[41m");
250 }
251
252 #[test]
253 fn test_color_to_ansi_ansi256_foreground() {
254 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
255 assert_eq!(result, "\x1b[38;5;200m");
256 }
257
258 #[test]
259 fn test_color_to_ansi_ansi256_background() {
260 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
261 assert_eq!(result, "\x1b[48;5;100m");
262 }
263
264 #[test]
265 fn test_color_to_ansi_rgb_foreground() {
266 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
267 assert_eq!(result, "\x1b[38;2;255;128;0m");
268 }
269
270 #[test]
271 fn test_color_to_ansi_rgb_background() {
272 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
273 assert_eq!(result, "\x1b[48;2;0;0;255m");
274 }
275
276 #[test]
277 fn test_color_to_ansi_rgb_zero_values() {
278 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
279 assert_eq!(result, "\x1b[38;2;0;0;0m");
280 }
281
282 #[test]
285 fn test_emphasis_to_ansi_bold() {
286 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
287 }
288
289 #[test]
290 fn test_emphasis_to_ansi_dim() {
291 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
292 }
293
294 #[test]
295 fn test_emphasis_to_ansi_italic() {
296 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
297 }
298
299 #[test]
300 fn test_emphasis_to_ansi_underline() {
301 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
302 }
303
304 #[test]
305 fn test_emphasis_to_ansi_blink() {
306 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
307 }
308
309 #[test]
310 fn test_emphasis_to_ansi_strikethrough() {
311 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
312 }
313
314 #[test]
317 fn test_style_to_ansi_empty_style_returns_empty_string() {
318 let style = Style {
319 fg: None,
320 bg: None,
321 bold: false,
322 dim: false,
323 italic: false,
324 underline: false,
325 strikethrough: false,
326 blink: false,
327 };
328 assert_eq!(style_to_ansi(&style), "");
329 }
330
331 #[test]
332 fn test_style_to_ansi_bold_only() {
333 let style = Style {
334 fg: None,
335 bg: None,
336 bold: true,
337 dim: false,
338 italic: false,
339 underline: false,
340 strikethrough: false,
341 blink: false,
342 };
343 assert_eq!(style_to_ansi(&style), "\x1b[1m");
344 }
345
346 #[test]
347 fn test_style_to_ansi_bold_with_foreground_color() {
348 let style = Style {
349 fg: Some(Color::Named(NamedColor::Green)),
350 bg: None,
351 bold: true,
352 dim: false,
353 italic: false,
354 underline: false,
355 strikethrough: false,
356 blink: false,
357 };
358 assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
359 }
360
361 #[test]
362 fn test_style_to_ansi_fg_and_bg() {
363 let style = Style {
364 fg: Some(Color::Named(NamedColor::White)),
365 bg: Some(Color::Named(NamedColor::Blue)),
366 bold: false,
367 dim: false,
368 italic: false,
369 underline: false,
370 strikethrough: false,
371 blink: false,
372 };
373 assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
374 }
375
376 #[test]
377 fn test_style_to_ansi_all_emphasis_flags() {
378 let style = Style {
379 fg: None,
380 bg: None,
381 bold: true,
382 dim: true,
383 italic: true,
384 underline: true,
385 strikethrough: true,
386 blink: true,
387 };
388 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
389 }
390}
391
392