1use std::fmt::Write;
12
13use crate::errors::LexError;
14use crate::lexer::{EmphasisType, TagType, Token, tokenize};
15
16#[derive(Debug, PartialEq, Clone)]
18pub enum Ground {
19 Foreground,
21 Background,
23}
24
25#[derive(Default, Clone)]
27pub struct Style {
28 pub fg: Option<Color>,
30 pub bg: Option<Color>,
32 pub bold: bool,
34 pub dim: bool,
36 pub italic: bool,
38 pub underline: bool,
40 pub strikethrough: bool,
42 pub blink: bool,
44 pub reset: bool,
46 pub prefix: Option<String>,
48}
49
50#[derive(Debug, PartialEq, Clone)]
52pub enum NamedColor {
53 Black,
54 Red,
55 Green,
56 Yellow,
57 Blue,
58 Magenta,
59 Cyan,
60 White,
61 BrightBlack,
62 BrightRed,
63 BrightGreen,
64 BrightYellow,
65 BrightBlue,
66 BrightMagenta,
67 BrightCyan,
68 BrightWhite,
69}
70
71#[derive(Debug, PartialEq, Clone)]
73pub enum Color {
74 Named(NamedColor),
75 Ansi256(u8),
76 Rgb(u8, u8, u8),
77}
78
79impl Style {
80 pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
98 let mut res = Self {
99 ..Default::default()
100 };
101 for tok in tokenize(markup.into())? {
102 match tok {
103 Token::Text(_) => continue,
104 Token::Tag(tag) => match tag {
105 TagType::Reset(_) => res.reset = true,
106 TagType::Emphasis(emphasis) => match emphasis {
107 EmphasisType::Dim => res.dim = true,
108 EmphasisType::Blink => res.blink = true,
109 EmphasisType::Bold => res.bold = true,
110 EmphasisType::Italic => res.italic = true,
111 EmphasisType::Strikethrough => res.strikethrough = true,
112 EmphasisType::Underline => res.underline = true,
113 },
114 TagType::Color { color, ground } => match ground {
115 Ground::Background => res.bg = Some(color),
116 Ground::Foreground => res.fg = Some(color),
117 },
118 TagType::Prefix(_) => continue,
119 },
120 }
121 }
122
123 Ok(res)
124 }
125}
126
127impl NamedColor {
128 pub(crate) fn from_str(input: &str) -> Option<Self> {
133 match input {
134 "black" => Some(Self::Black),
135 "red" => Some(Self::Red),
136 "green" => Some(Self::Green),
137 "yellow" => Some(Self::Yellow),
138 "blue" => Some(Self::Blue),
139 "magenta" => Some(Self::Magenta),
140 "cyan" => Some(Self::Cyan),
141 "white" => Some(Self::White),
142 "bright-black" => Some(Self::BrightBlack),
143 "bright-red" => Some(Self::BrightRed),
144 "bright-green" => Some(Self::BrightGreen),
145 "bright-yellow" => Some(Self::BrightYellow),
146 "bright-blue" => Some(Self::BrightBlue),
147 "bright-magenta" => Some(Self::BrightMagenta),
148 "bright-cyan" => Some(Self::BrightCyan),
149 "bright-white" => Some(Self::BrightWhite),
150 _ => None,
151 }
152 }
153}
154
155fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
159 let mut seq = String::from("\x1b[");
160
161 for (i, n) in vec.iter().enumerate() {
162 if i != 0 {
163 seq.push(';');
164 }
165 write!(seq, "{n}").unwrap();
166 }
167
168 seq.push('m');
169 seq
170}
171
172fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
175 let addend: u8 = match param {
176 Ground::Background => 10,
177 Ground::Foreground => 0,
178 };
179 match color {
180 Color::Named(named) => {
181 ansi.push(match named {
182 NamedColor::Black => 30 + addend,
183 NamedColor::Red => 31 + addend,
184 NamedColor::Green => 32 + addend,
185 NamedColor::Yellow => 33 + addend,
186 NamedColor::Blue => 34 + addend,
187 NamedColor::Magenta => 35 + addend,
188 NamedColor::Cyan => 36 + addend,
189 NamedColor::White => 37 + addend,
190 NamedColor::BrightBlack => 90 + addend,
191 NamedColor::BrightRed => 91 + addend,
192 NamedColor::BrightGreen => 92 + addend,
193 NamedColor::BrightYellow => 93 + addend,
194 NamedColor::BrightBlue => 94 + addend,
195 NamedColor::BrightMagenta => 95 + addend,
196 NamedColor::BrightCyan => 96 + addend,
197 NamedColor::BrightWhite => 97 + addend,
198 });
199 }
200 Color::Ansi256(v) => {
201 ansi.extend_from_slice(&[38 + addend, 5, *v]);
202 }
203 Color::Rgb(r, g, b) => {
204 ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
205 }
206 }
207}
208
209pub fn color_to_ansi(color: &Color, ground: Ground) -> String {
217 let mut ansi: Vec<u8> = Vec::new();
218 encode_color_sgr(&mut ansi, ground, color);
219
220 vec_to_ansi_seq(ansi)
221}
222
223pub fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
225 let code = match emphasis {
226 EmphasisType::Bold => 1,
227 EmphasisType::Dim => 2,
228 EmphasisType::Italic => 3,
229 EmphasisType::Underline => 4,
230 EmphasisType::Blink => 5,
231 EmphasisType::Strikethrough => 9,
232 };
233 vec_to_ansi_seq(vec![code])
234}
235
236pub fn style_to_ansi(style: &Style) -> String {
256 let mut ansi: Vec<u8> = Vec::new();
257
258 if style.reset {
259 return String::from("\x1b[0m");
260 }
261
262 for (enabled, code) in [
263 (style.bold, 1),
264 (style.dim, 2),
265 (style.italic, 3),
266 (style.underline, 4),
267 (style.blink, 5),
268 (style.strikethrough, 9),
269 ] {
270 if enabled {
271 ansi.push(code);
272 }
273 }
274
275 if let Some(fg) = &style.fg {
276 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
277 }
278 if let Some(bg) = &style.bg {
279 encode_color_sgr(&mut ansi, Ground::Background, bg);
280 }
281
282 if ansi.is_empty() {
283 return String::new();
284 }
285
286 vec_to_ansi_seq(ansi)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::lexer::EmphasisType;
293
294 #[test]
297 fn test_named_color_from_str_known_colors() {
298 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
299 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
300 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
301 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
302 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
303 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
304 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
305 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
306 }
307
308 #[test]
309 fn test_named_color_from_str_unknown_returns_none() {
310 assert_eq!(NamedColor::from_str("purple"), None);
311 }
312
313 #[test]
314 fn test_named_color_from_str_case_sensitive() {
315 assert_eq!(NamedColor::from_str("Red"), None);
316 assert_eq!(NamedColor::from_str("RED"), None);
317 }
318
319 #[test]
320 fn test_named_color_from_str_empty_returns_none() {
321 assert_eq!(NamedColor::from_str(""), None);
322 }
323
324 #[test]
327 fn test_vec_to_ansi_seq_single_param() {
328 let result = vec_to_ansi_seq(vec![1]);
329 assert_eq!(result, "\x1b[1m");
330 }
331
332 #[test]
333 fn test_vec_to_ansi_seq_multiple_params() {
334 let result = vec_to_ansi_seq(vec![1, 31]);
335 assert_eq!(result, "\x1b[1;31m");
336 }
337
338 #[test]
339 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
340 let result = vec_to_ansi_seq(vec![]);
341 assert_eq!(result, "\x1b[m");
342 }
343
344 #[test]
347 fn test_color_to_ansi_named_foreground() {
348 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
349 assert_eq!(result, "\x1b[31m");
350 }
351
352 #[test]
353 fn test_color_to_ansi_named_background() {
354 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
355 assert_eq!(result, "\x1b[41m");
356 }
357
358 #[test]
359 fn test_color_to_ansi_ansi256_foreground() {
360 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
361 assert_eq!(result, "\x1b[38;5;200m");
362 }
363
364 #[test]
365 fn test_color_to_ansi_ansi256_background() {
366 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
367 assert_eq!(result, "\x1b[48;5;100m");
368 }
369
370 #[test]
371 fn test_color_to_ansi_rgb_foreground() {
372 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
373 assert_eq!(result, "\x1b[38;2;255;128;0m");
374 }
375
376 #[test]
377 fn test_color_to_ansi_rgb_background() {
378 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
379 assert_eq!(result, "\x1b[48;2;0;0;255m");
380 }
381
382 #[test]
383 fn test_color_to_ansi_rgb_zero_values() {
384 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
385 assert_eq!(result, "\x1b[38;2;0;0;0m");
386 }
387
388 #[test]
391 fn test_emphasis_to_ansi_bold() {
392 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
393 }
394
395 #[test]
396 fn test_emphasis_to_ansi_dim() {
397 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
398 }
399
400 #[test]
401 fn test_emphasis_to_ansi_italic() {
402 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
403 }
404
405 #[test]
406 fn test_emphasis_to_ansi_underline() {
407 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
408 }
409
410 #[test]
411 fn test_emphasis_to_ansi_blink() {
412 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
413 }
414
415 #[test]
416 fn test_emphasis_to_ansi_strikethrough() {
417 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
418 }
419
420 #[test]
423 fn test_style_to_ansi_empty_style_returns_empty_string() {
424 let style = Style {
425 fg: None,
426 bg: None,
427 bold: false,
428 dim: false,
429 italic: false,
430 underline: false,
431 strikethrough: false,
432 blink: false,
433 ..Default::default()
434 };
435 assert_eq!(style_to_ansi(&style), "");
436 }
437
438 #[test]
439 fn test_style_to_ansi_bold_only() {
440 let style = Style {
441 fg: None,
442 bg: None,
443 bold: true,
444 dim: false,
445 italic: false,
446 underline: false,
447 strikethrough: false,
448 blink: false,
449 ..Default::default()
450 };
451 assert_eq!(style_to_ansi(&style), "\x1b[1m");
452 }
453
454 #[test]
455 fn test_style_to_ansi_bold_with_foreground_color() {
456 let style = Style {
457 fg: Some(Color::Named(NamedColor::Green)),
458 bg: None,
459 bold: true,
460 dim: false,
461 italic: false,
462 underline: false,
463 strikethrough: false,
464 blink: false,
465 ..Default::default()
466 };
467 assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
468 }
469
470 #[test]
471 fn test_style_to_ansi_fg_and_bg() {
472 let style = Style {
473 fg: Some(Color::Named(NamedColor::White)),
474 bg: Some(Color::Named(NamedColor::Blue)),
475 bold: false,
476 dim: false,
477 italic: false,
478 underline: false,
479 strikethrough: false,
480 blink: false,
481 ..Default::default()
482 };
483 assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
484 }
485
486 #[test]
487 fn test_style_to_ansi_all_emphasis_flags() {
488 let style = Style {
489 fg: None,
490 bg: None,
491 bold: true,
492 dim: true,
493 italic: true,
494 underline: true,
495 strikethrough: true,
496 blink: true,
497 ..Default::default()
498 };
499 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
500 }
501}
502
503