1use std::fmt::Write;
12
13use crate::errors::LexError;
14use crate::lexer::{tokenize, EmphasisType, TagType, Token};
15
16#[derive(Debug, PartialEq, Clone)]
18pub enum Ground {
19 Foreground,
21 Background,
23}
24
25#[derive(Default, Clone, Debug)]
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 double_underline: bool,
42 pub strikethrough: bool,
44 pub blink: bool,
46 pub overline: bool,
48 pub invisible: bool,
50 pub reverse: bool,
52 pub rapid_blink: bool,
54 pub reset: bool,
56 pub prefix: Option<String>,
58}
59
60#[derive(Debug, PartialEq, Clone)]
62pub enum NamedColor {
63 Black,
64 Red,
65 Green,
66 Yellow,
67 Blue,
68 Magenta,
69 Cyan,
70 White,
71 BrightBlack,
72 BrightRed,
73 BrightGreen,
74 BrightYellow,
75 BrightBlue,
76 BrightMagenta,
77 BrightCyan,
78 BrightWhite,
79}
80
81#[derive(Debug, PartialEq, Clone)]
83pub enum Color {
84 Named(NamedColor),
85 Ansi256(u8),
86 Rgb(u8, u8, u8),
87}
88
89impl Style {
90 pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
108 let mut res = Self {
109 ..Default::default()
110 };
111 for tok in tokenize(markup.into())? {
112 match tok {
113 Token::Text(_) => continue,
114 Token::Tag(tag) => match tag {
115 TagType::ResetAll | TagType::ResetOne(_) => res.reset = true,
116 TagType::Emphasis(emphasis) => match emphasis {
117 EmphasisType::Dim => res.dim = true,
118 EmphasisType::Blink => res.blink = true,
119 EmphasisType::Bold => res.bold = true,
120 EmphasisType::Italic => res.italic = true,
121 EmphasisType::Strikethrough => res.strikethrough = true,
122 EmphasisType::Underline => res.underline = true,
123 EmphasisType::DoubleUnderline => res.double_underline = true,
124 EmphasisType::Overline => res.overline = true,
125 EmphasisType::Invisible => res.invisible = true,
126 EmphasisType::Reverse => res.reverse = true,
127 EmphasisType::RapidBlink => res.rapid_blink = true,
128 },
129 TagType::Color { color, ground } => match ground {
130 Ground::Background => res.bg = Some(color),
131 Ground::Foreground => res.fg = Some(color),
132 },
133 TagType::Prefix(_) => continue,
134 },
135 }
136 }
137
138 Ok(res)
139 }
140}
141
142impl NamedColor {
143 pub(crate) fn from_str(input: &str) -> Option<Self> {
148 match input {
149 "black" => Some(Self::Black),
150 "red" => Some(Self::Red),
151 "green" => Some(Self::Green),
152 "yellow" => Some(Self::Yellow),
153 "blue" => Some(Self::Blue),
154 "magenta" => Some(Self::Magenta),
155 "cyan" => Some(Self::Cyan),
156 "white" => Some(Self::White),
157 "bright-black" => Some(Self::BrightBlack),
158 "bright-red" => Some(Self::BrightRed),
159 "bright-green" => Some(Self::BrightGreen),
160 "bright-yellow" => Some(Self::BrightYellow),
161 "bright-blue" => Some(Self::BrightBlue),
162 "bright-magenta" => Some(Self::BrightMagenta),
163 "bright-cyan" => Some(Self::BrightCyan),
164 "bright-white" => Some(Self::BrightWhite),
165 _ => None,
166 }
167 }
168}
169
170fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
174 let mut seq = String::from("\x1b[");
175
176 for (i, n) in vec.iter().enumerate() {
177 if i != 0 {
178 seq.push(';');
179 }
180 write!(seq, "{n}").unwrap();
181 }
182
183 seq.push('m');
184 seq
185}
186
187fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
190 let addend: u8 = match param {
191 Ground::Background => 10,
192 Ground::Foreground => 0,
193 };
194 match color {
195 Color::Named(named) => {
196 ansi.push(match named {
197 NamedColor::Black => 30 + addend,
198 NamedColor::Red => 31 + addend,
199 NamedColor::Green => 32 + addend,
200 NamedColor::Yellow => 33 + addend,
201 NamedColor::Blue => 34 + addend,
202 NamedColor::Magenta => 35 + addend,
203 NamedColor::Cyan => 36 + addend,
204 NamedColor::White => 37 + addend,
205 NamedColor::BrightBlack => 90 + addend,
206 NamedColor::BrightRed => 91 + addend,
207 NamedColor::BrightGreen => 92 + addend,
208 NamedColor::BrightYellow => 93 + addend,
209 NamedColor::BrightBlue => 94 + addend,
210 NamedColor::BrightMagenta => 95 + addend,
211 NamedColor::BrightCyan => 96 + addend,
212 NamedColor::BrightWhite => 97 + addend,
213 });
214 }
215 Color::Ansi256(v) => {
216 ansi.extend_from_slice(&[38 + addend, 5, *v]);
217 }
218 Color::Rgb(r, g, b) => {
219 ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
220 }
221 }
222}
223
224const fn named_sgr(color: &NamedColor) -> u8 {
226 match color {
227 NamedColor::Black => 30,
228 NamedColor::Red => 31,
229 NamedColor::Green => 32,
230 NamedColor::Yellow => 33,
231 NamedColor::Blue => 34,
232 NamedColor::Magenta => 35,
233 NamedColor::Cyan => 36,
234 NamedColor::White => 37,
235 NamedColor::BrightBlack => 90,
236 NamedColor::BrightRed => 91,
237 NamedColor::BrightGreen => 92,
238 NamedColor::BrightYellow => 93,
239 NamedColor::BrightBlue => 94,
240 NamedColor::BrightMagenta => 95,
241 NamedColor::BrightCyan => 96,
242 NamedColor::BrightWhite => 97,
243 }
244}
245
246pub fn color_to_ansi(color: &Color, ground: Ground) -> String {
254 let add: u8 = match ground {
255 Ground::Background => 10,
256 Ground::Foreground => 0,
257 };
258 match color {
259 Color::Named(n) => format!("\x1b[{}m", named_sgr(n) + add),
260 Color::Ansi256(v) => format!("\x1b[{};5;{}m", 38 + add, v),
261 Color::Rgb(r, g, b) => format!("\x1b[{};2;{};{};{}m", 38 + add, r, g, b),
262 }
263}
264
265pub fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
267 let code: u8 = match emphasis {
268 EmphasisType::Bold => 1,
269 EmphasisType::Dim => 2,
270 EmphasisType::Italic => 3,
271 EmphasisType::Underline => 4,
272 EmphasisType::DoubleUnderline => 21,
273 EmphasisType::Blink => 5,
274 EmphasisType::RapidBlink => 6,
275 EmphasisType::Reverse => 7,
276 EmphasisType::Invisible => 8,
277 EmphasisType::Strikethrough => 9,
278 EmphasisType::Overline => 53,
279 };
280 format!("\x1b[{}m", code)
281}
282
283pub fn style_to_ansi(style: &Style) -> String {
303 let mut ansi: Vec<u8> = Vec::new();
304
305 if style.reset {
306 return String::from("\x1b[0m");
307 }
308
309 for (enabled, code) in [
310 (style.bold, 1),
311 (style.dim, 2),
312 (style.italic, 3),
313 (style.underline, 4),
314 (style.double_underline, 21),
315 (style.blink, 5),
316 (style.rapid_blink, 6),
317 (style.reverse, 7),
318 (style.invisible, 8),
319 (style.strikethrough, 9),
320 (style.overline, 53),
321 ] {
322 if enabled {
323 ansi.push(code);
324 }
325 }
326
327 if let Some(fg) = &style.fg {
328 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
329 }
330 if let Some(bg) = &style.bg {
331 encode_color_sgr(&mut ansi, Ground::Background, bg);
332 }
333
334 if ansi.is_empty() {
335 return String::new();
336 }
337
338 vec_to_ansi_seq(ansi)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::lexer::EmphasisType;
345
346 #[test]
349 fn test_named_color_from_str_known_colors() {
350 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
351 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
352 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
353 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
354 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
355 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
356 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
357 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
358 }
359
360 #[test]
361 fn test_named_color_from_str_unknown_returns_none() {
362 assert_eq!(NamedColor::from_str("purple"), None);
363 }
364
365 #[test]
366 fn test_named_color_from_str_case_sensitive() {
367 assert_eq!(NamedColor::from_str("Red"), None);
368 assert_eq!(NamedColor::from_str("RED"), None);
369 }
370
371 #[test]
372 fn test_named_color_from_str_empty_returns_none() {
373 assert_eq!(NamedColor::from_str(""), None);
374 }
375
376 #[test]
379 fn test_vec_to_ansi_seq_single_param() {
380 let result = vec_to_ansi_seq(vec![1]);
381 assert_eq!(result, "\x1b[1m");
382 }
383
384 #[test]
385 fn test_vec_to_ansi_seq_multiple_params() {
386 let result = vec_to_ansi_seq(vec![1, 31]);
387 assert_eq!(result, "\x1b[1;31m");
388 }
389
390 #[test]
391 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
392 let result = vec_to_ansi_seq(vec![]);
393 assert_eq!(result, "\x1b[m");
394 }
395
396 #[test]
399 fn test_color_to_ansi_named_foreground() {
400 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
401 assert_eq!(result, "\x1b[31m");
402 }
403
404 #[test]
405 fn test_color_to_ansi_named_background() {
406 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
407 assert_eq!(result, "\x1b[41m");
408 }
409
410 #[test]
411 fn test_color_to_ansi_ansi256_foreground() {
412 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
413 assert_eq!(result, "\x1b[38;5;200m");
414 }
415
416 #[test]
417 fn test_color_to_ansi_ansi256_background() {
418 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
419 assert_eq!(result, "\x1b[48;5;100m");
420 }
421
422 #[test]
423 fn test_color_to_ansi_rgb_foreground() {
424 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
425 assert_eq!(result, "\x1b[38;2;255;128;0m");
426 }
427
428 #[test]
429 fn test_color_to_ansi_rgb_background() {
430 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
431 assert_eq!(result, "\x1b[48;2;0;0;255m");
432 }
433
434 #[test]
435 fn test_color_to_ansi_rgb_zero_values() {
436 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
437 assert_eq!(result, "\x1b[38;2;0;0;0m");
438 }
439
440 #[test]
443 fn test_emphasis_to_ansi_bold() {
444 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
445 }
446
447 #[test]
448 fn test_emphasis_to_ansi_dim() {
449 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
450 }
451
452 #[test]
453 fn test_emphasis_to_ansi_italic() {
454 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
455 }
456
457 #[test]
458 fn test_emphasis_to_ansi_underline() {
459 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
460 }
461
462 #[test]
463 fn test_emphasis_to_ansi_blink() {
464 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
465 }
466
467 #[test]
468 fn test_emphasis_to_ansi_strikethrough() {
469 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
470 }
471
472 #[test]
475 fn test_style_to_ansi_empty_style_returns_empty_string() {
476 let style = Style {
477 fg: None,
478 bg: None,
479 bold: false,
480 dim: false,
481 italic: false,
482 underline: false,
483 strikethrough: false,
484 blink: false,
485 ..Default::default()
486 };
487 assert_eq!(style_to_ansi(&style), "");
488 }
489
490 #[test]
491 fn test_style_to_ansi_bold_only() {
492 let style = Style {
493 fg: None,
494 bg: None,
495 bold: true,
496 dim: false,
497 italic: false,
498 underline: false,
499 strikethrough: false,
500 blink: false,
501 ..Default::default()
502 };
503 assert_eq!(style_to_ansi(&style), "\x1b[1m");
504 }
505
506 #[test]
507 fn test_style_to_ansi_bold_with_foreground_color() {
508 let style = Style {
509 fg: Some(Color::Named(NamedColor::Green)),
510 bg: None,
511 bold: true,
512 dim: false,
513 italic: false,
514 underline: false,
515 strikethrough: false,
516 blink: false,
517 ..Default::default()
518 };
519 assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
520 }
521
522 #[test]
523 fn test_style_to_ansi_fg_and_bg() {
524 let style = Style {
525 fg: Some(Color::Named(NamedColor::White)),
526 bg: Some(Color::Named(NamedColor::Blue)),
527 bold: false,
528 dim: false,
529 italic: false,
530 underline: false,
531 strikethrough: false,
532 blink: false,
533 ..Default::default()
534 };
535 assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
536 }
537
538 #[test]
539 fn test_style_to_ansi_all_emphasis_flags() {
540 let style = Style {
541 fg: None,
542 bg: None,
543 bold: true,
544 dim: true,
545 italic: true,
546 underline: true,
547 strikethrough: true,
548 blink: true,
549 ..Default::default()
550 };
551 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
552 }
553}
554
555