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,
65 Red,
67 Green,
69 Yellow,
71 Blue,
73 Magenta,
75 Cyan,
77 White,
79 BrightBlack,
81 BrightRed,
83 BrightGreen,
85 BrightYellow,
87 BrightBlue,
89 BrightMagenta,
91 BrightCyan,
93 BrightWhite,
95}
96
97#[derive(Debug, PartialEq, Clone)]
99pub enum Color {
100 Named(NamedColor),
102 Ansi256(u8),
104 Rgb(u8, u8, u8),
106}
107
108impl Style {
109 pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
127 let mut res = Self {
128 ..Default::default()
129 };
130 for tok in tokenize(markup.into())? {
131 match tok {
132 Token::Text(_) => continue,
133 Token::Tag(tag) => match tag {
134 TagType::ResetAll | TagType::ResetOne(_) => res.reset = true,
135 TagType::Emphasis(emphasis) => match emphasis {
136 EmphasisType::Dim => res.dim = true,
137 EmphasisType::Blink => res.blink = true,
138 EmphasisType::Bold => res.bold = true,
139 EmphasisType::Italic => res.italic = true,
140 EmphasisType::Strikethrough => res.strikethrough = true,
141 EmphasisType::Underline => res.underline = true,
142 EmphasisType::DoubleUnderline => res.double_underline = true,
143 EmphasisType::Overline => res.overline = true,
144 EmphasisType::Invisible => res.invisible = true,
145 EmphasisType::Reverse => res.reverse = true,
146 EmphasisType::RapidBlink => res.rapid_blink = true,
147 },
148 TagType::Color { color, ground } => match ground {
149 Ground::Background => res.bg = Some(color),
150 Ground::Foreground => res.fg = Some(color),
151 },
152 TagType::Prefix(_) => continue,
153 },
154 }
155 }
156
157 Ok(res)
158 }
159}
160
161impl NamedColor {
162 pub(crate) fn from_str(input: &str) -> Option<Self> {
167 match input {
168 "black" => Some(Self::Black),
169 "red" => Some(Self::Red),
170 "green" => Some(Self::Green),
171 "yellow" => Some(Self::Yellow),
172 "blue" => Some(Self::Blue),
173 "magenta" => Some(Self::Magenta),
174 "cyan" => Some(Self::Cyan),
175 "white" => Some(Self::White),
176 "bright-black" => Some(Self::BrightBlack),
177 "bright-red" => Some(Self::BrightRed),
178 "bright-green" => Some(Self::BrightGreen),
179 "bright-yellow" => Some(Self::BrightYellow),
180 "bright-blue" => Some(Self::BrightBlue),
181 "bright-magenta" => Some(Self::BrightMagenta),
182 "bright-cyan" => Some(Self::BrightCyan),
183 "bright-white" => Some(Self::BrightWhite),
184 _ => None,
185 }
186 }
187}
188
189fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
193 let mut seq = String::from("\x1b[");
194
195 for (i, n) in vec.iter().enumerate() {
196 if i != 0 {
197 seq.push(';');
198 }
199 write!(seq, "{n}").unwrap();
200 }
201
202 seq.push('m');
203 seq
204}
205
206fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
209 let addend: u8 = match param {
210 Ground::Background => 10,
211 Ground::Foreground => 0,
212 };
213 match color {
214 Color::Named(named) => {
215 ansi.push(match named {
216 NamedColor::Black => 30 + addend,
217 NamedColor::Red => 31 + addend,
218 NamedColor::Green => 32 + addend,
219 NamedColor::Yellow => 33 + addend,
220 NamedColor::Blue => 34 + addend,
221 NamedColor::Magenta => 35 + addend,
222 NamedColor::Cyan => 36 + addend,
223 NamedColor::White => 37 + addend,
224 NamedColor::BrightBlack => 90 + addend,
225 NamedColor::BrightRed => 91 + addend,
226 NamedColor::BrightGreen => 92 + addend,
227 NamedColor::BrightYellow => 93 + addend,
228 NamedColor::BrightBlue => 94 + addend,
229 NamedColor::BrightMagenta => 95 + addend,
230 NamedColor::BrightCyan => 96 + addend,
231 NamedColor::BrightWhite => 97 + addend,
232 });
233 }
234 Color::Ansi256(v) => {
235 ansi.extend_from_slice(&[38 + addend, 5, *v]);
236 }
237 Color::Rgb(r, g, b) => {
238 ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
239 }
240 }
241}
242
243const fn named_sgr(color: &NamedColor) -> u8 {
245 match color {
246 NamedColor::Black => 30,
247 NamedColor::Red => 31,
248 NamedColor::Green => 32,
249 NamedColor::Yellow => 33,
250 NamedColor::Blue => 34,
251 NamedColor::Magenta => 35,
252 NamedColor::Cyan => 36,
253 NamedColor::White => 37,
254 NamedColor::BrightBlack => 90,
255 NamedColor::BrightRed => 91,
256 NamedColor::BrightGreen => 92,
257 NamedColor::BrightYellow => 93,
258 NamedColor::BrightBlue => 94,
259 NamedColor::BrightMagenta => 95,
260 NamedColor::BrightCyan => 96,
261 NamedColor::BrightWhite => 97,
262 }
263}
264
265pub fn color_to_ansi(color: &Color, ground: Ground) -> String {
276 let add: u8 = match ground {
277 Ground::Background => 10,
278 Ground::Foreground => 0,
279 };
280 match color {
281 Color::Named(n) => format!("\x1b[{}m", named_sgr(n) + add),
282 Color::Ansi256(v) => format!("\x1b[{};5;{}m", 38 + add, v),
283 Color::Rgb(r, g, b) => format!("\x1b[{};2;{};{};{}m", 38 + add, r, g, b),
284 }
285}
286
287pub fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
291 let code: u8 = match emphasis {
292 EmphasisType::Bold => 1,
293 EmphasisType::Dim => 2,
294 EmphasisType::Italic => 3,
295 EmphasisType::Underline => 4,
296 EmphasisType::DoubleUnderline => 21,
297 EmphasisType::Blink => 5,
298 EmphasisType::RapidBlink => 6,
299 EmphasisType::Reverse => 7,
300 EmphasisType::Invisible => 8,
301 EmphasisType::Strikethrough => 9,
302 EmphasisType::Overline => 53,
303 };
304 format!("\x1b[{}m", code)
305}
306
307pub fn style_to_ansi(style: &Style) -> String {
327 let mut ansi: Vec<u8> = Vec::new();
328
329 if style.reset {
330 return String::from("\x1b[0m");
331 }
332
333 for (enabled, code) in [
334 (style.bold, 1),
335 (style.dim, 2),
336 (style.italic, 3),
337 (style.underline, 4),
338 (style.double_underline, 21),
339 (style.blink, 5),
340 (style.rapid_blink, 6),
341 (style.reverse, 7),
342 (style.invisible, 8),
343 (style.strikethrough, 9),
344 (style.overline, 53),
345 ] {
346 if enabled {
347 ansi.push(code);
348 }
349 }
350
351 if let Some(fg) = &style.fg {
352 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
353 }
354 if let Some(bg) = &style.bg {
355 encode_color_sgr(&mut ansi, Ground::Background, bg);
356 }
357
358 if ansi.is_empty() {
359 return String::new();
360 }
361
362 vec_to_ansi_seq(ansi)
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::lexer::EmphasisType;
369
370 #[test]
373 fn test_named_color_from_str_known_colors() {
374 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
375 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
376 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
377 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
378 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
379 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
380 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
381 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
382 }
383
384 #[test]
385 fn test_named_color_from_str_unknown_returns_none() {
386 assert_eq!(NamedColor::from_str("purple"), None);
387 }
388
389 #[test]
390 fn test_named_color_from_str_case_sensitive() {
391 assert_eq!(NamedColor::from_str("Red"), None);
392 assert_eq!(NamedColor::from_str("RED"), None);
393 }
394
395 #[test]
396 fn test_named_color_from_str_empty_returns_none() {
397 assert_eq!(NamedColor::from_str(""), None);
398 }
399
400 #[test]
403 fn test_vec_to_ansi_seq_single_param() {
404 let result = vec_to_ansi_seq(vec![1]);
405 assert_eq!(result, "\x1b[1m");
406 }
407
408 #[test]
409 fn test_vec_to_ansi_seq_multiple_params() {
410 let result = vec_to_ansi_seq(vec![1, 31]);
411 assert_eq!(result, "\x1b[1;31m");
412 }
413
414 #[test]
415 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
416 let result = vec_to_ansi_seq(vec![]);
417 assert_eq!(result, "\x1b[m");
418 }
419
420 #[test]
423 fn test_color_to_ansi_named_foreground() {
424 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
425 assert_eq!(result, "\x1b[31m");
426 }
427
428 #[test]
429 fn test_color_to_ansi_named_background() {
430 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
431 assert_eq!(result, "\x1b[41m");
432 }
433
434 #[test]
435 fn test_color_to_ansi_ansi256_foreground() {
436 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
437 assert_eq!(result, "\x1b[38;5;200m");
438 }
439
440 #[test]
441 fn test_color_to_ansi_ansi256_background() {
442 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
443 assert_eq!(result, "\x1b[48;5;100m");
444 }
445
446 #[test]
447 fn test_color_to_ansi_rgb_foreground() {
448 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
449 assert_eq!(result, "\x1b[38;2;255;128;0m");
450 }
451
452 #[test]
453 fn test_color_to_ansi_rgb_background() {
454 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
455 assert_eq!(result, "\x1b[48;2;0;0;255m");
456 }
457
458 #[test]
459 fn test_color_to_ansi_rgb_zero_values() {
460 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
461 assert_eq!(result, "\x1b[38;2;0;0;0m");
462 }
463
464 #[test]
467 fn test_emphasis_to_ansi_bold() {
468 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
469 }
470
471 #[test]
472 fn test_emphasis_to_ansi_dim() {
473 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
474 }
475
476 #[test]
477 fn test_emphasis_to_ansi_italic() {
478 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
479 }
480
481 #[test]
482 fn test_emphasis_to_ansi_underline() {
483 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
484 }
485
486 #[test]
487 fn test_emphasis_to_ansi_blink() {
488 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
489 }
490
491 #[test]
492 fn test_emphasis_to_ansi_strikethrough() {
493 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
494 }
495
496 #[test]
499 fn test_style_to_ansi_empty_style_returns_empty_string() {
500 let style = Style {
501 fg: None,
502 bg: None,
503 bold: false,
504 dim: false,
505 italic: false,
506 underline: false,
507 strikethrough: false,
508 blink: false,
509 ..Default::default()
510 };
511 assert_eq!(style_to_ansi(&style), "");
512 }
513
514 #[test]
515 fn test_style_to_ansi_bold_only() {
516 let style = Style {
517 fg: None,
518 bg: None,
519 bold: true,
520 dim: false,
521 italic: false,
522 underline: false,
523 strikethrough: false,
524 blink: false,
525 ..Default::default()
526 };
527 assert_eq!(style_to_ansi(&style), "\x1b[1m");
528 }
529
530 #[test]
531 fn test_style_to_ansi_bold_with_foreground_color() {
532 let style = Style {
533 fg: Some(Color::Named(NamedColor::Green)),
534 bg: None,
535 bold: true,
536 dim: false,
537 italic: false,
538 underline: false,
539 strikethrough: false,
540 blink: false,
541 ..Default::default()
542 };
543 assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
544 }
545
546 #[test]
547 fn test_style_to_ansi_fg_and_bg() {
548 let style = Style {
549 fg: Some(Color::Named(NamedColor::White)),
550 bg: Some(Color::Named(NamedColor::Blue)),
551 bold: false,
552 dim: false,
553 italic: false,
554 underline: false,
555 strikethrough: false,
556 blink: false,
557 ..Default::default()
558 };
559 assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
560 }
561
562 #[test]
563 fn test_style_to_ansi_all_emphasis_flags() {
564 let style = Style {
565 fg: None,
566 bg: None,
567 bold: true,
568 dim: true,
569 italic: true,
570 underline: true,
571 strikethrough: true,
572 blink: true,
573 ..Default::default()
574 };
575 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
576 }
577}
578
579