1use std::fmt::Write;
2
3use crate::errors::LexError;
4use crate::lexer::{EmphasisType, TagType, Token, tokenize};
5
6#[derive(Debug, PartialEq)]
8pub enum Ground {
9 Foreground,
11 Background,
13}
14
15#[derive(Default, Clone)]
17pub struct Style {
18 pub fg: Option<Color>,
20 pub bg: Option<Color>,
22 pub bold: bool,
24 pub dim: bool,
26 pub italic: bool,
28 pub underline: bool,
30 pub strikethrough: bool,
32 pub blink: bool,
34 pub reset: bool,
36}
37
38#[derive(Debug, PartialEq, Clone)]
40pub enum NamedColor {
41 Black,
42 Red,
43 Green,
44 Yellow,
45 Blue,
46 Magenta,
47 Cyan,
48 White,
49}
50
51#[derive(Debug, PartialEq, Clone)]
53pub enum Color {
54 Named(NamedColor),
55 Ansi256(u8),
56 Rgb(u8, u8, u8),
57}
58
59impl Style {
60 pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
61 let mut res = Self {
62 ..Default::default()
63 };
64 for tok in tokenize(markup.into())? {
65 match tok {
66 Token::Text(_) => continue,
67 Token::Tag(tag) => match tag {
68 TagType::Reset => res.reset = true,
69 TagType::Emphasis(emphasis) => match emphasis {
70 EmphasisType::Dim => res.dim = true,
71 EmphasisType::Blink => res.blink = true,
72 EmphasisType::Bold => res.bold = true,
73 EmphasisType::Italic => res.italic = true,
74 EmphasisType::Strikethrough => res.strikethrough = true,
75 EmphasisType::Underline => res.underline = true,
76 },
77 TagType::Color { color, ground } => match ground {
78 Ground::Background => res.bg = Some(color),
79 Ground::Foreground => res.fg = Some(color),
80 },
81 },
82 }
83 }
84
85 Ok(res)
86 }
87}
88
89impl NamedColor {
90 pub(crate) fn from_str(input: &str) -> Option<Self> {
95 match input {
96 "black" => Some(Self::Black),
97 "red" => Some(Self::Red),
98 "green" => Some(Self::Green),
99 "yellow" => Some(Self::Yellow),
100 "blue" => Some(Self::Blue),
101 "magenta" => Some(Self::Magenta),
102 "cyan" => Some(Self::Cyan),
103 "white" => Some(Self::White),
104 _ => None,
105 }
106 }
107}
108
109fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
113 let mut seq = String::from("\x1b[");
114
115 for (i, n) in vec.iter().enumerate() {
116 if i != 0 {
117 seq.push(';');
118 }
119 write!(seq, "{n}").unwrap();
120 }
121
122 seq.push('m');
123 seq
124}
125
126fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
129 let addend: u8 = match param {
130 Ground::Background => 10,
131 Ground::Foreground => 0,
132 };
133 match color {
134 Color::Named(named) => {
135 ansi.push(match named {
136 NamedColor::Black => 30 + addend,
137 NamedColor::Red => 31 + addend,
138 NamedColor::Green => 32 + addend,
139 NamedColor::Yellow => 33 + addend,
140 NamedColor::Blue => 34 + addend,
141 NamedColor::Magenta => 35 + addend,
142 NamedColor::Cyan => 36 + addend,
143 NamedColor::White => 37 + addend,
144 });
145 }
146 Color::Ansi256(v) => {
147 ansi.extend_from_slice(&[38 + addend, 5, *v]);
148 }
149 Color::Rgb(r, g, b) => {
150 ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
151 }
152 }
153}
154
155pub(crate) fn color_to_ansi(color: &Color, ground: Ground) -> String {
163 let mut ansi: Vec<u8> = Vec::new();
164 encode_color_sgr(&mut ansi, ground, color);
165
166 vec_to_ansi_seq(ansi)
167}
168
169pub(crate) fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
171 let code = match emphasis {
172 EmphasisType::Bold => 1,
173 EmphasisType::Dim => 2,
174 EmphasisType::Italic => 3,
175 EmphasisType::Underline => 4,
176 EmphasisType::Blink => 5,
177 EmphasisType::Strikethrough => 9,
178 };
179 vec_to_ansi_seq(vec![code])
180}
181
182pub(crate) fn style_to_ansi(style: &Style) -> String {
187 let mut ansi: Vec<u8> = Vec::new();
188
189 if style.reset {
190 return String::from("\x1b[0m");
191 }
192
193 for (enabled, code) in [
194 (style.bold, 1),
195 (style.dim, 2),
196 (style.italic, 3),
197 (style.underline, 4),
198 (style.blink, 5),
199 (style.strikethrough, 9),
200 ] {
201 if enabled {
202 ansi.push(code);
203 }
204 }
205
206 if let Some(fg) = &style.fg {
207 encode_color_sgr(&mut ansi, Ground::Foreground, fg);
208 }
209 if let Some(bg) = &style.bg {
210 encode_color_sgr(&mut ansi, Ground::Background, bg);
211 }
212
213 if ansi.is_empty() {
214 return String::new();
215 }
216
217 vec_to_ansi_seq(ansi)
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::lexer::EmphasisType;
224
225 #[test]
228 fn test_named_color_from_str_known_colors() {
229 assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
230 assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
231 assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
232 assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
233 assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
234 assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
235 assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
236 assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
237 }
238
239 #[test]
240 fn test_named_color_from_str_unknown_returns_none() {
241 assert_eq!(NamedColor::from_str("purple"), None);
242 }
243
244 #[test]
245 fn test_named_color_from_str_case_sensitive() {
246 assert_eq!(NamedColor::from_str("Red"), None);
247 assert_eq!(NamedColor::from_str("RED"), None);
248 }
249
250 #[test]
251 fn test_named_color_from_str_empty_returns_none() {
252 assert_eq!(NamedColor::from_str(""), None);
253 }
254
255 #[test]
258 fn test_vec_to_ansi_seq_single_param() {
259 let result = vec_to_ansi_seq(vec![1]);
260 assert_eq!(result, "\x1b[1m");
261 }
262
263 #[test]
264 fn test_vec_to_ansi_seq_multiple_params() {
265 let result = vec_to_ansi_seq(vec![1, 31]);
266 assert_eq!(result, "\x1b[1;31m");
267 }
268
269 #[test]
270 fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
271 let result = vec_to_ansi_seq(vec![]);
272 assert_eq!(result, "\x1b[m");
273 }
274
275 #[test]
278 fn test_color_to_ansi_named_foreground() {
279 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
280 assert_eq!(result, "\x1b[31m");
281 }
282
283 #[test]
284 fn test_color_to_ansi_named_background() {
285 let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
286 assert_eq!(result, "\x1b[41m");
287 }
288
289 #[test]
290 fn test_color_to_ansi_ansi256_foreground() {
291 let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
292 assert_eq!(result, "\x1b[38;5;200m");
293 }
294
295 #[test]
296 fn test_color_to_ansi_ansi256_background() {
297 let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
298 assert_eq!(result, "\x1b[48;5;100m");
299 }
300
301 #[test]
302 fn test_color_to_ansi_rgb_foreground() {
303 let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
304 assert_eq!(result, "\x1b[38;2;255;128;0m");
305 }
306
307 #[test]
308 fn test_color_to_ansi_rgb_background() {
309 let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
310 assert_eq!(result, "\x1b[48;2;0;0;255m");
311 }
312
313 #[test]
314 fn test_color_to_ansi_rgb_zero_values() {
315 let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
316 assert_eq!(result, "\x1b[38;2;0;0;0m");
317 }
318
319 #[test]
322 fn test_emphasis_to_ansi_bold() {
323 assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
324 }
325
326 #[test]
327 fn test_emphasis_to_ansi_dim() {
328 assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
329 }
330
331 #[test]
332 fn test_emphasis_to_ansi_italic() {
333 assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
334 }
335
336 #[test]
337 fn test_emphasis_to_ansi_underline() {
338 assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
339 }
340
341 #[test]
342 fn test_emphasis_to_ansi_blink() {
343 assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
344 }
345
346 #[test]
347 fn test_emphasis_to_ansi_strikethrough() {
348 assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
349 }
350
351 #[test]
354 fn test_style_to_ansi_empty_style_returns_empty_string() {
355 let style = Style {
356 fg: None,
357 bg: None,
358 bold: false,
359 dim: false,
360 italic: false,
361 underline: false,
362 strikethrough: false,
363 blink: false,
364 ..Default::default()
365 };
366 assert_eq!(style_to_ansi(&style), "");
367 }
368
369 #[test]
370 fn test_style_to_ansi_bold_only() {
371 let style = Style {
372 fg: None,
373 bg: None,
374 bold: true,
375 dim: false,
376 italic: false,
377 underline: false,
378 strikethrough: false,
379 blink: false,
380 ..Default::default()
381 };
382 assert_eq!(style_to_ansi(&style), "\x1b[1m");
383 }
384
385 #[test]
386 fn test_style_to_ansi_bold_with_foreground_color() {
387 let style = Style {
388 fg: Some(Color::Named(NamedColor::Green)),
389 bg: None,
390 bold: true,
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), "\x1b[1;32m");
399 }
400
401 #[test]
402 fn test_style_to_ansi_fg_and_bg() {
403 let style = Style {
404 fg: Some(Color::Named(NamedColor::White)),
405 bg: Some(Color::Named(NamedColor::Blue)),
406 bold: false,
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[37;44m");
415 }
416
417 #[test]
418 fn test_style_to_ansi_all_emphasis_flags() {
419 let style = Style {
420 fg: None,
421 bg: None,
422 bold: true,
423 dim: true,
424 italic: true,
425 underline: true,
426 strikethrough: true,
427 blink: true,
428 ..Default::default()
429 };
430 assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
431 }
432}
433
434