1use std::fmt::Write;
12
13use crate::errors::LexError;
14use crate::lexer::{EmphasisType, TagType, Token, tokenize};
15
16#[derive(Debug, PartialEq)]
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}
62
63#[derive(Debug, PartialEq, Clone)]
65pub enum Color {
66 Named(NamedColor),
67 Ansi256(u8),
68 Rgb(u8, u8, u8),
69}
70
71impl Style {
72 pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
90 let mut res = Self {
91 ..Default::default()
92 };
93 for tok in tokenize(markup.into())? {
94 match tok {
95 Token::Text(_) => continue,
96 Token::Tag(tag) => match tag {
97 TagType::Reset => res.reset = true,
98 TagType::Emphasis(emphasis) => match emphasis {
99 EmphasisType::Dim => res.dim = true,
100 EmphasisType::Blink => res.blink = true,
101 EmphasisType::Bold => res.bold = true,
102 EmphasisType::Italic => res.italic = true,
103 EmphasisType::Strikethrough => res.strikethrough = true,
104 EmphasisType::Underline => res.underline = true,
105 },
106 TagType::Color { color, ground } => match ground {
107 Ground::Background => res.bg = Some(color),
108 Ground::Foreground => res.fg = Some(color),
109 },
110 TagType::Prefix(_) => continue,
111 },
112 }
113 }
114
115 Ok(res)
116 }
117}
118
119impl NamedColor {
120 pub(crate) fn from_str(input: &str) -> Option<Self> {
125 match input {
126 "black" => Some(Self::Black),
127 "red" => Some(Self::Red),
128 "green" => Some(Self::Green),
129 "yellow" => Some(Self::Yellow),
130 "blue" => Some(Self::Blue),
131 "magenta" => Some(Self::Magenta),
132 "cyan" => Some(Self::Cyan),
133 "white" => Some(Self::White),
134 _ => None,
135 }
136 }
137}
138
139fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
143 let mut seq = String::from("\x1b[");
144
145 for (i, n) in vec.iter().enumerate() {
146 if i != 0 {
147 seq.push(';');
148 }
149 write!(seq, "{n}").unwrap();
150 }
151
152 seq.push('m');
153 seq
154}
155
156fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
159 let addend: u8 = match param {
160 Ground::Background => 10,
161 Ground::Foreground => 0,
162 };
163 match color {
164 Color::Named(named) => {
165 ansi.push(match named {
166 NamedColor::Black => 30 + addend,
167 NamedColor::Red => 31 + addend,
168 NamedColor::Green => 32 + addend,
169 NamedColor::Yellow => 33 + addend,
170 NamedColor::Blue => 34 + addend,
171 NamedColor::Magenta => 35 + addend,
172 NamedColor::Cyan => 36 + addend,
173 NamedColor::White => 37 + addend,
174 });
175 }
176 Color::Ansi256(v) => {
177 ansi.extend_from_slice(&[38 + addend, 5, *v]);
178 }
179 Color::Rgb(r, g, b) => {
180 ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
181 }
182 }
183}
184
185pub(crate) fn color_to_ansi(color: &Color, ground: Ground) -> String {
193 let mut ansi: Vec<u8> = Vec::new();
194 encode_color_sgr(&mut ansi, ground, color);
195
196 vec_to_ansi_seq(ansi)
197}
198
199pub(crate) fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
201 let code = match emphasis {
202 EmphasisType::Bold => 1,
203 EmphasisType::Dim => 2,
204 EmphasisType::Italic => 3,
205 EmphasisType::Underline => 4,
206 EmphasisType::Blink => 5,
207 EmphasisType::Strikethrough => 9,
208 };
209 vec_to_ansi_seq(vec![code])
210}
211
212pub(crate) fn style_to_ansi(style: &Style) -> String {
219 let mut ansi: Vec<u8> = Vec::new();
220
221 if style.reset {
222 return String::from("\x1b[0m");
223 }
224
225 for (enabled, code) in [
226 (style.bold, 1),
227 (style.dim, 2),
228 (style.italic, 3),
229 (style.underline, 4),
230 (style.blink, 5),
231 (style.strikethrough, 9),
232 ] {
233 if enabled {
234 ansi.push(code);
235 }
236 }
237
238 if let Some(fg) = &style.fg {
239 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
240 }
241 if let Some(bg) = &style.bg {
242 encode_color_sgr(&mut ansi, Ground::Background, bg);
243 }
244
245 if ansi.is_empty() {
246 return String::new();
247 }
248
249 vec_to_ansi_seq(ansi)
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::lexer::EmphasisType;
256
257 #[test]
260 fn test_named_color_from_str_known_colors() {
261 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
262 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
263 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
264 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
265 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
266 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
267 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
268 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
269 }
270
271 #[test]
272 fn test_named_color_from_str_unknown_returns_none() {
273 assert_eq!(NamedColor::from_str("purple"), None);
274 }
275
276 #[test]
277 fn test_named_color_from_str_case_sensitive() {
278 assert_eq!(NamedColor::from_str("Red"), None);
279 assert_eq!(NamedColor::from_str("RED"), None);
280 }
281
282 #[test]
283 fn test_named_color_from_str_empty_returns_none() {
284 assert_eq!(NamedColor::from_str(""), None);
285 }
286
287 #[test]
290 fn test_vec_to_ansi_seq_single_param() {
291 let result = vec_to_ansi_seq(vec![1]);
292 assert_eq!(result, "\x1b[1m");
293 }
294
295 #[test]
296 fn test_vec_to_ansi_seq_multiple_params() {
297 let result = vec_to_ansi_seq(vec![1, 31]);
298 assert_eq!(result, "\x1b[1;31m");
299 }
300
301 #[test]
302 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
303 let result = vec_to_ansi_seq(vec![]);
304 assert_eq!(result, "\x1b[m");
305 }
306
307 #[test]
310 fn test_color_to_ansi_named_foreground() {
311 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
312 assert_eq!(result, "\x1b[31m");
313 }
314
315 #[test]
316 fn test_color_to_ansi_named_background() {
317 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
318 assert_eq!(result, "\x1b[41m");
319 }
320
321 #[test]
322 fn test_color_to_ansi_ansi256_foreground() {
323 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
324 assert_eq!(result, "\x1b[38;5;200m");
325 }
326
327 #[test]
328 fn test_color_to_ansi_ansi256_background() {
329 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
330 assert_eq!(result, "\x1b[48;5;100m");
331 }
332
333 #[test]
334 fn test_color_to_ansi_rgb_foreground() {
335 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
336 assert_eq!(result, "\x1b[38;2;255;128;0m");
337 }
338
339 #[test]
340 fn test_color_to_ansi_rgb_background() {
341 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
342 assert_eq!(result, "\x1b[48;2;0;0;255m");
343 }
344
345 #[test]
346 fn test_color_to_ansi_rgb_zero_values() {
347 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
348 assert_eq!(result, "\x1b[38;2;0;0;0m");
349 }
350
351 #[test]
354 fn test_emphasis_to_ansi_bold() {
355 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
356 }
357
358 #[test]
359 fn test_emphasis_to_ansi_dim() {
360 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
361 }
362
363 #[test]
364 fn test_emphasis_to_ansi_italic() {
365 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
366 }
367
368 #[test]
369 fn test_emphasis_to_ansi_underline() {
370 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
371 }
372
373 #[test]
374 fn test_emphasis_to_ansi_blink() {
375 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
376 }
377
378 #[test]
379 fn test_emphasis_to_ansi_strikethrough() {
380 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
381 }
382
383 #[test]
386 fn test_style_to_ansi_empty_style_returns_empty_string() {
387 let style = Style {
388 fg: None,
389 bg: None,
390 bold: false,
391 dim: false,
392 italic: false,
393 underline: false,
394 strikethrough: false,
395 blink: false,
396 ..Default::default()
397 };
398 assert_eq!(style_to_ansi(&style), "");
399 }
400
401 #[test]
402 fn test_style_to_ansi_bold_only() {
403 let style = Style {
404 fg: None,
405 bg: None,
406 bold: true,
407 dim: false,
408 italic: false,
409 underline: false,
410 strikethrough: false,
411 blink: false,
412 ..Default::default()
413 };
414 assert_eq!(style_to_ansi(&style), "\x1b[1m");
415 }
416
417 #[test]
418 fn test_style_to_ansi_bold_with_foreground_color() {
419 let style = Style {
420 fg: Some(Color::Named(NamedColor::Green)),
421 bg: None,
422 bold: true,
423 dim: false,
424 italic: false,
425 underline: false,
426 strikethrough: false,
427 blink: false,
428 ..Default::default()
429 };
430 assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
431 }
432
433 #[test]
434 fn test_style_to_ansi_fg_and_bg() {
435 let style = Style {
436 fg: Some(Color::Named(NamedColor::White)),
437 bg: Some(Color::Named(NamedColor::Blue)),
438 bold: false,
439 dim: false,
440 italic: false,
441 underline: false,
442 strikethrough: false,
443 blink: false,
444 ..Default::default()
445 };
446 assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
447 }
448
449 #[test]
450 fn test_style_to_ansi_all_emphasis_flags() {
451 let style = Style {
452 fg: None,
453 bg: None,
454 bold: true,
455 dim: true,
456 italic: true,
457 underline: true,
458 strikethrough: true,
459 blink: true,
460 ..Default::default()
461 };
462 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
463 }
464}
465
466