1use std::fmt::Write;
12
13use crate::errors::LexError;
14use crate::lexer::{EmphasisType, TagType, Token, tokenize};
15
16#[derive(Debug, PartialEq, Clone, Copy)]
18pub enum Ground {
19 Foreground,
21 Background,
23}
24
25#[derive(Default, Clone, Debug)]
27#[allow(clippy::struct_excessive_bools)]
28pub struct Style {
29 pub fg: Option<Color>,
31 pub bg: Option<Color>,
33 pub bold: bool,
35 pub dim: bool,
37 pub italic: bool,
39 pub underline: bool,
41 pub double_underline: bool,
43 pub strikethrough: bool,
45 pub blink: bool,
47 pub overline: bool,
49 pub invisible: bool,
51 pub reverse: bool,
53 pub rapid_blink: bool,
55 pub reset: bool,
57 pub prefix: Option<String>,
59}
60
61#[derive(Debug, PartialEq, Clone)]
63pub enum NamedColor {
64 Black,
66 Red,
68 Green,
70 Yellow,
72 Blue,
74 Magenta,
76 Cyan,
78 White,
80 BrightBlack,
82 BrightRed,
84 BrightGreen,
86 BrightYellow,
88 BrightBlue,
90 BrightMagenta,
92 BrightCyan,
94 BrightWhite,
96}
97
98#[derive(Debug, PartialEq, Clone)]
100pub enum Color {
101 Named(NamedColor),
103 Ansi256(u8),
105 Rgb(u8, u8, u8),
107}
108
109impl Style {
110 pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
128 let mut res = Self {
129 ..Default::default()
130 };
131 for tok in tokenize(markup.into())? {
132 match tok {
133 Token::Text(_) => {}
134 Token::Tag(tag) => match tag {
135 TagType::ResetAll | TagType::ResetOne(_) => res.reset = true,
136 TagType::Emphasis(emphasis) => match emphasis {
137 EmphasisType::Dim => res.dim = true,
138 EmphasisType::Blink => res.blink = true,
139 EmphasisType::Bold => res.bold = true,
140 EmphasisType::Italic => res.italic = true,
141 EmphasisType::Strikethrough => res.strikethrough = true,
142 EmphasisType::Underline => res.underline = true,
143 EmphasisType::DoubleUnderline => res.double_underline = true,
144 EmphasisType::Overline => res.overline = true,
145 EmphasisType::Invisible => res.invisible = true,
146 EmphasisType::Reverse => res.reverse = true,
147 EmphasisType::RapidBlink => res.rapid_blink = true,
148 },
149 TagType::Color { color, ground } => match ground {
150 Ground::Background => res.bg = Some(color),
151 Ground::Foreground => res.fg = Some(color),
152 },
153 TagType::Prefix(_) => {}
154 },
155 }
156 }
157
158 Ok(res)
159 }
160}
161
162impl NamedColor {
163 pub(crate) fn from_str(input: &str) -> Option<Self> {
168 match input {
169 "black" => Some(Self::Black),
170 "red" => Some(Self::Red),
171 "green" => Some(Self::Green),
172 "yellow" => Some(Self::Yellow),
173 "blue" => Some(Self::Blue),
174 "magenta" => Some(Self::Magenta),
175 "cyan" => Some(Self::Cyan),
176 "white" => Some(Self::White),
177 "bright-black" => Some(Self::BrightBlack),
178 "bright-red" => Some(Self::BrightRed),
179 "bright-green" => Some(Self::BrightGreen),
180 "bright-yellow" => Some(Self::BrightYellow),
181 "bright-blue" => Some(Self::BrightBlue),
182 "bright-magenta" => Some(Self::BrightMagenta),
183 "bright-cyan" => Some(Self::BrightCyan),
184 "bright-white" => Some(Self::BrightWhite),
185 _ => None,
186 }
187 }
188}
189
190fn vec_to_ansi_seq(vec: &[u8]) -> String {
194 let mut seq = String::from("\x1b[");
195
196 for (i, n) in vec.iter().enumerate() {
197 if i != 0 {
198 seq.push(';');
199 }
200 write!(seq, "{n}").unwrap();
201 }
202
203 seq.push('m');
204 seq
205}
206
207fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
210 let addend: u8 = match param {
211 Ground::Background => 10,
212 Ground::Foreground => 0,
213 };
214 match color {
215 Color::Named(named) => {
216 ansi.push(match named {
217 NamedColor::Black => 30 + addend,
218 NamedColor::Red => 31 + addend,
219 NamedColor::Green => 32 + addend,
220 NamedColor::Yellow => 33 + addend,
221 NamedColor::Blue => 34 + addend,
222 NamedColor::Magenta => 35 + addend,
223 NamedColor::Cyan => 36 + addend,
224 NamedColor::White => 37 + addend,
225 NamedColor::BrightBlack => 90 + addend,
226 NamedColor::BrightRed => 91 + addend,
227 NamedColor::BrightGreen => 92 + addend,
228 NamedColor::BrightYellow => 93 + addend,
229 NamedColor::BrightBlue => 94 + addend,
230 NamedColor::BrightMagenta => 95 + addend,
231 NamedColor::BrightCyan => 96 + addend,
232 NamedColor::BrightWhite => 97 + addend,
233 });
234 }
235 Color::Ansi256(v) => {
236 ansi.extend_from_slice(&[38 + addend, 5, *v]);
237 }
238 Color::Rgb(r, g, b) => {
239 ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
240 }
241 }
242}
243
244const fn named_sgr(color: &NamedColor) -> u8 {
246 match color {
247 NamedColor::Black => 30,
248 NamedColor::Red => 31,
249 NamedColor::Green => 32,
250 NamedColor::Yellow => 33,
251 NamedColor::Blue => 34,
252 NamedColor::Magenta => 35,
253 NamedColor::Cyan => 36,
254 NamedColor::White => 37,
255 NamedColor::BrightBlack => 90,
256 NamedColor::BrightRed => 91,
257 NamedColor::BrightGreen => 92,
258 NamedColor::BrightYellow => 93,
259 NamedColor::BrightBlue => 94,
260 NamedColor::BrightMagenta => 95,
261 NamedColor::BrightCyan => 96,
262 NamedColor::BrightWhite => 97,
263 }
264}
265
266#[must_use]
277pub fn color_to_ansi(color: &Color, ground: Ground) -> String {
278 let add: u8 = match ground {
279 Ground::Background => 10,
280 Ground::Foreground => 0,
281 };
282 match color {
283 Color::Named(n) => format!("\x1b[{}m", named_sgr(n) + add),
284 Color::Ansi256(v) => format!("\x1b[{};5;{}m", 38 + add, v),
285 Color::Rgb(r, g, b) => format!("\x1b[{};2;{};{};{}m", 38 + add, r, g, b),
286 }
287}
288
289#[must_use]
293pub fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
294 let code: u8 = match emphasis {
295 EmphasisType::Bold => 1,
296 EmphasisType::Dim => 2,
297 EmphasisType::Italic => 3,
298 EmphasisType::Underline => 4,
299 EmphasisType::DoubleUnderline => 21,
300 EmphasisType::Blink => 5,
301 EmphasisType::RapidBlink => 6,
302 EmphasisType::Reverse => 7,
303 EmphasisType::Invisible => 8,
304 EmphasisType::Strikethrough => 9,
305 EmphasisType::Overline => 53,
306 };
307 format!("\x1b[{code}m")
308}
309
310pub fn write_color_ansi(output: &mut String, color: &Color, ground: Ground) {
314 let add: u8 = match ground {
315 Ground::Background => 10,
316 Ground::Foreground => 0,
317 };
318 match color {
319 Color::Named(n) => {
320 let _ = write!(output, "\x1b[{}m", named_sgr(n) + add);
321 }
322 Color::Ansi256(v) => {
323 let _ = write!(output, "\x1b[{};5;{}m", 38 + add, v);
324 }
325 Color::Rgb(r, g, b) => {
326 let _ = write!(output, "\x1b[{};2;{};{};{}m", 38 + add, r, g, b);
327 }
328 }
329}
330
331pub fn write_emphasis_ansi(output: &mut String, emphasis: &EmphasisType) {
335 let code: u8 = match emphasis {
336 EmphasisType::Bold => 1,
337 EmphasisType::Dim => 2,
338 EmphasisType::Italic => 3,
339 EmphasisType::Underline => 4,
340 EmphasisType::DoubleUnderline => 21,
341 EmphasisType::Blink => 5,
342 EmphasisType::RapidBlink => 6,
343 EmphasisType::Reverse => 7,
344 EmphasisType::Invisible => 8,
345 EmphasisType::Strikethrough => 9,
346 EmphasisType::Overline => 53,
347 };
348 let _ = write!(output, "\x1b[{code}m");
349}
350
351#[must_use]
371pub fn style_to_ansi(style: &Style) -> String {
372 let mut ansi: Vec<u8> = Vec::new();
373
374 if style.reset {
375 return String::from("\x1b[0m");
376 }
377
378 for (enabled, code) in [
379 (style.bold, 1),
380 (style.dim, 2),
381 (style.italic, 3),
382 (style.underline, 4),
383 (style.double_underline, 21),
384 (style.blink, 5),
385 (style.rapid_blink, 6),
386 (style.reverse, 7),
387 (style.invisible, 8),
388 (style.strikethrough, 9),
389 (style.overline, 53),
390 ] {
391 if enabled {
392 ansi.push(code);
393 }
394 }
395
396 if let Some(fg) = &style.fg {
397 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
398 }
399 if let Some(bg) = &style.bg {
400 encode_color_sgr(&mut ansi, Ground::Background, bg);
401 }
402
403 if ansi.is_empty() {
404 return String::new();
405 }
406
407 vec_to_ansi_seq(&ansi)
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::lexer::EmphasisType;
414
415 #[test]
418 fn test_named_color_from_str_known_colors() {
419 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
420 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
421 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
422 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
423 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
424 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
425 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
426 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
427 }
428
429 #[test]
430 fn test_named_color_from_str_unknown_returns_none() {
431 assert_eq!(NamedColor::from_str("purple"), None);
432 }
433
434 #[test]
435 fn test_named_color_from_str_case_sensitive() {
436 assert_eq!(NamedColor::from_str("Red"), None);
437 assert_eq!(NamedColor::from_str("RED"), None);
438 }
439
440 #[test]
441 fn test_named_color_from_str_empty_returns_none() {
442 assert_eq!(NamedColor::from_str(""), None);
443 }
444
445 #[test]
448 fn test_vec_to_ansi_seq_single_param() {
449 let result = vec_to_ansi_seq(&[1]);
450 assert_eq!(result, "\x1b[1m");
451 }
452
453 #[test]
454 fn test_vec_to_ansi_seq_multiple_params() {
455 let result = vec_to_ansi_seq(&[1, 31]);
456 assert_eq!(result, "\x1b[1;31m");
457 }
458
459 #[test]
460 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
461 let result = vec_to_ansi_seq(&[]);
462 assert_eq!(result, "\x1b[m");
463 }
464
465 #[test]
468 fn test_color_to_ansi_named_foreground() {
469 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
470 assert_eq!(result, "\x1b[31m");
471 }
472
473 #[test]
474 fn test_color_to_ansi_named_background() {
475 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
476 assert_eq!(result, "\x1b[41m");
477 }
478
479 #[test]
480 fn test_color_to_ansi_ansi256_foreground() {
481 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
482 assert_eq!(result, "\x1b[38;5;200m");
483 }
484
485 #[test]
486 fn test_color_to_ansi_ansi256_background() {
487 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
488 assert_eq!(result, "\x1b[48;5;100m");
489 }
490
491 #[test]
492 fn test_color_to_ansi_rgb_foreground() {
493 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
494 assert_eq!(result, "\x1b[38;2;255;128;0m");
495 }
496
497 #[test]
498 fn test_color_to_ansi_rgb_background() {
499 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
500 assert_eq!(result, "\x1b[48;2;0;0;255m");
501 }
502
503 #[test]
504 fn test_color_to_ansi_rgb_zero_values() {
505 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
506 assert_eq!(result, "\x1b[38;2;0;0;0m");
507 }
508
509 #[test]
512 fn test_emphasis_to_ansi_bold() {
513 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
514 }
515
516 #[test]
517 fn test_emphasis_to_ansi_dim() {
518 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
519 }
520
521 #[test]
522 fn test_emphasis_to_ansi_italic() {
523 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
524 }
525
526 #[test]
527 fn test_emphasis_to_ansi_underline() {
528 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
529 }
530
531 #[test]
532 fn test_emphasis_to_ansi_blink() {
533 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
534 }
535
536 #[test]
537 fn test_emphasis_to_ansi_strikethrough() {
538 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
539 }
540
541 #[test]
544 fn test_style_to_ansi_empty_style_returns_empty_string() {
545 let style = Style {
546 fg: None,
547 bg: None,
548 bold: false,
549 dim: false,
550 italic: false,
551 underline: false,
552 strikethrough: false,
553 blink: false,
554 ..Default::default()
555 };
556 assert_eq!(style_to_ansi(&style), "");
557 }
558
559 #[test]
560 fn test_style_to_ansi_bold_only() {
561 let style = Style {
562 fg: None,
563 bg: None,
564 bold: true,
565 dim: false,
566 italic: false,
567 underline: false,
568 strikethrough: false,
569 blink: false,
570 ..Default::default()
571 };
572 assert_eq!(style_to_ansi(&style), "\x1b[1m");
573 }
574
575 #[test]
576 fn test_style_to_ansi_bold_with_foreground_color() {
577 let style = Style {
578 fg: Some(Color::Named(NamedColor::Green)),
579 bg: None,
580 bold: true,
581 dim: false,
582 italic: false,
583 underline: false,
584 strikethrough: false,
585 blink: false,
586 ..Default::default()
587 };
588 assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
589 }
590
591 #[test]
592 fn test_style_to_ansi_fg_and_bg() {
593 let style = Style {
594 fg: Some(Color::Named(NamedColor::White)),
595 bg: Some(Color::Named(NamedColor::Blue)),
596 bold: false,
597 dim: false,
598 italic: false,
599 underline: false,
600 strikethrough: false,
601 blink: false,
602 ..Default::default()
603 };
604 assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
605 }
606
607 #[test]
608 fn test_style_to_ansi_all_emphasis_flags() {
609 let style = Style {
610 fg: None,
611 bg: None,
612 bold: true,
613 dim: true,
614 italic: true,
615 underline: true,
616 strikethrough: true,
617 blink: true,
618 ..Default::default()
619 };
620 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
621 }
622}
623
624