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(crate) 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(crate) 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
236#[allow(unused)]
243pub(crate) fn style_to_ansi(style: &Style) -> String {
244 let mut ansi: Vec<u8> = Vec::new();
245
246 if style.reset {
247 return String::from("\x1b[0m");
248 }
249
250 for (enabled, code) in [
251 (style.bold, 1),
252 (style.dim, 2),
253 (style.italic, 3),
254 (style.underline, 4),
255 (style.blink, 5),
256 (style.strikethrough, 9),
257 ] {
258 if enabled {
259 ansi.push(code);
260 }
261 }
262
263 if let Some(fg) = &style.fg {
264 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
265 }
266 if let Some(bg) = &style.bg {
267 encode_color_sgr(&mut ansi, Ground::Background, bg);
268 }
269
270 if ansi.is_empty() {
271 return String::new();
272 }
273
274 vec_to_ansi_seq(ansi)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::lexer::EmphasisType;
281
282 #[test]
285 fn test_named_color_from_str_known_colors() {
286 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
287 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
288 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
289 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
290 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
291 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
292 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
293 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
294 }
295
296 #[test]
297 fn test_named_color_from_str_unknown_returns_none() {
298 assert_eq!(NamedColor::from_str("purple"), None);
299 }
300
301 #[test]
302 fn test_named_color_from_str_case_sensitive() {
303 assert_eq!(NamedColor::from_str("Red"), None);
304 assert_eq!(NamedColor::from_str("RED"), None);
305 }
306
307 #[test]
308 fn test_named_color_from_str_empty_returns_none() {
309 assert_eq!(NamedColor::from_str(""), None);
310 }
311
312 #[test]
315 fn test_vec_to_ansi_seq_single_param() {
316 let result = vec_to_ansi_seq(vec![1]);
317 assert_eq!(result, "\x1b[1m");
318 }
319
320 #[test]
321 fn test_vec_to_ansi_seq_multiple_params() {
322 let result = vec_to_ansi_seq(vec![1, 31]);
323 assert_eq!(result, "\x1b[1;31m");
324 }
325
326 #[test]
327 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
328 let result = vec_to_ansi_seq(vec![]);
329 assert_eq!(result, "\x1b[m");
330 }
331
332 #[test]
335 fn test_color_to_ansi_named_foreground() {
336 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
337 assert_eq!(result, "\x1b[31m");
338 }
339
340 #[test]
341 fn test_color_to_ansi_named_background() {
342 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
343 assert_eq!(result, "\x1b[41m");
344 }
345
346 #[test]
347 fn test_color_to_ansi_ansi256_foreground() {
348 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
349 assert_eq!(result, "\x1b[38;5;200m");
350 }
351
352 #[test]
353 fn test_color_to_ansi_ansi256_background() {
354 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
355 assert_eq!(result, "\x1b[48;5;100m");
356 }
357
358 #[test]
359 fn test_color_to_ansi_rgb_foreground() {
360 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
361 assert_eq!(result, "\x1b[38;2;255;128;0m");
362 }
363
364 #[test]
365 fn test_color_to_ansi_rgb_background() {
366 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
367 assert_eq!(result, "\x1b[48;2;0;0;255m");
368 }
369
370 #[test]
371 fn test_color_to_ansi_rgb_zero_values() {
372 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
373 assert_eq!(result, "\x1b[38;2;0;0;0m");
374 }
375
376 #[test]
379 fn test_emphasis_to_ansi_bold() {
380 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
381 }
382
383 #[test]
384 fn test_emphasis_to_ansi_dim() {
385 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
386 }
387
388 #[test]
389 fn test_emphasis_to_ansi_italic() {
390 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
391 }
392
393 #[test]
394 fn test_emphasis_to_ansi_underline() {
395 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
396 }
397
398 #[test]
399 fn test_emphasis_to_ansi_blink() {
400 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
401 }
402
403 #[test]
404 fn test_emphasis_to_ansi_strikethrough() {
405 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
406 }
407
408 #[test]
411 fn test_style_to_ansi_empty_style_returns_empty_string() {
412 let style = Style {
413 fg: None,
414 bg: None,
415 bold: false,
416 dim: false,
417 italic: false,
418 underline: false,
419 strikethrough: false,
420 blink: false,
421 ..Default::default()
422 };
423 assert_eq!(style_to_ansi(&style), "");
424 }
425
426 #[test]
427 fn test_style_to_ansi_bold_only() {
428 let style = Style {
429 fg: None,
430 bg: None,
431 bold: true,
432 dim: false,
433 italic: false,
434 underline: false,
435 strikethrough: false,
436 blink: false,
437 ..Default::default()
438 };
439 assert_eq!(style_to_ansi(&style), "\x1b[1m");
440 }
441
442 #[test]
443 fn test_style_to_ansi_bold_with_foreground_color() {
444 let style = Style {
445 fg: Some(Color::Named(NamedColor::Green)),
446 bg: None,
447 bold: true,
448 dim: false,
449 italic: false,
450 underline: false,
451 strikethrough: false,
452 blink: false,
453 ..Default::default()
454 };
455 assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
456 }
457
458 #[test]
459 fn test_style_to_ansi_fg_and_bg() {
460 let style = Style {
461 fg: Some(Color::Named(NamedColor::White)),
462 bg: Some(Color::Named(NamedColor::Blue)),
463 bold: false,
464 dim: false,
465 italic: false,
466 underline: false,
467 strikethrough: false,
468 blink: false,
469 ..Default::default()
470 };
471 assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
472 }
473
474 #[test]
475 fn test_style_to_ansi_all_emphasis_flags() {
476 let style = Style {
477 fg: None,
478 bg: None,
479 bold: true,
480 dim: true,
481 italic: true,
482 underline: true,
483 strikethrough: true,
484 blink: true,
485 ..Default::default()
486 };
487 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
488 }
489}
490
491