Skip to main content

farben_core/
parser.rs

1use crate::ansi::{Ground, color_to_ansi, emphasis_to_ansi};
2use crate::lexer::{TagType, Token};
3
4/// Renders a token stream into a raw ANSI-escaped string.
5///
6/// Text tokens are appended as-is. Tag tokens are converted to their corresponding
7/// ANSI escape sequences. Does not append a trailing reset; callers are responsible
8/// for that if needed.
9pub fn render(tokens: Vec<Token>) -> String {
10    let mut result = String::new();
11    for tok in tokens {
12        match tok {
13            Token::Text(text) => result.push_str(text.as_str()),
14            Token::Tag(tag) => match tag {
15                TagType::Color(color) => {
16                    result.push_str(color_to_ansi(&color, Ground::Foreground).as_str())
17                }
18                TagType::Emphasis(emphasis) => {
19                    result.push_str(emphasis_to_ansi(&emphasis).as_str())
20                }
21                TagType::Reset => result.push_str("\x1b[0m"),
22            },
23        }
24    }
25
26    result
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use crate::ansi::{Color, NamedColor};
33    use crate::lexer::{EmphasisType, TagType, Token};
34
35    // --- render ---
36
37    #[test]
38    fn test_render_empty_token_list() {
39        let result = render(vec![]);
40        assert_eq!(result, "");
41    }
42
43    #[test]
44    fn test_render_plain_text_token() {
45        let result = render(vec![Token::Text("hello".into())]);
46        assert_eq!(result, "hello");
47    }
48
49    #[test]
50    fn test_render_named_color_tag() {
51        let result = render(vec![Token::Tag(TagType::Color(Color::Named(
52            NamedColor::Red,
53        )))]);
54        assert_eq!(result, "\x1b[31m");
55    }
56
57    #[test]
58    fn test_render_emphasis_tag_bold() {
59        let result = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
60        assert_eq!(result, "\x1b[1m");
61    }
62
63    #[test]
64    fn test_render_reset_tag() {
65        let result = render(vec![Token::Tag(TagType::Reset)]);
66        assert_eq!(result, "\x1b[0m");
67    }
68
69    #[test]
70    fn test_render_color_then_text() {
71        let result = render(vec![
72            Token::Tag(TagType::Color(Color::Named(NamedColor::Red))),
73            Token::Text("hello".into()),
74        ]);
75        assert_eq!(result, "\x1b[31mhello");
76    }
77
78    #[test]
79    fn test_render_color_text_reset() {
80        let result = render(vec![
81            Token::Tag(TagType::Color(Color::Named(NamedColor::Green))),
82            Token::Text("go".into()),
83            Token::Tag(TagType::Reset),
84        ]);
85        assert_eq!(result, "\x1b[32mgo\x1b[0m");
86    }
87
88    #[test]
89    fn test_render_multiple_text_tokens() {
90        let result = render(vec![Token::Text("foo".into()), Token::Text("bar".into())]);
91        assert_eq!(result, "foobar");
92    }
93
94    #[test]
95    fn test_render_ansi256_color_tag() {
96        let result = render(vec![Token::Tag(TagType::Color(Color::Ansi256(21)))]);
97        assert_eq!(result, "\x1b[38;5;21m");
98    }
99
100    #[test]
101    fn test_render_rgb_color_tag() {
102        let result = render(vec![Token::Tag(TagType::Color(Color::Rgb(255, 0, 0)))]);
103        assert_eq!(result, "\x1b[38;2;255;0;0m");
104    }
105
106    #[test]
107    fn test_render_does_not_append_trailing_reset() {
108        // render() itself never appends a reset; that is the caller's job (try_color)
109        let result = render(vec![Token::Text("plain".into())]);
110        assert!(!result.ends_with("\x1b[0m"));
111    }
112}
113
114// Skipped (side effects): none: render() is a pure function.