Skip to main content

farben_core/
parser.rs

1//! Token stream renderer.
2//!
3//! Converts a sequence of [`Token`] values produced by the lexer into a final
4//! ANSI-escaped string ready for terminal output. This module is the last stage
5//! in the farben pipeline: tokenize with [`crate::lexer::tokenize`], then render
6//! with [`render`].
7
8use crate::ansi::{color_to_ansi, emphasis_to_ansi};
9use crate::lexer::{TagType, Token};
10
11/// Renders a token stream into a raw ANSI-escaped string.
12///
13/// Text tokens are appended as-is. Tag tokens are converted to their corresponding
14/// ANSI escape sequences. Does not append a trailing reset; callers are responsible
15/// for that if needed.
16///
17/// # Example
18///
19/// ```ignore
20/// let tokens = tokenize("[red]hello[/]")?;
21/// let output = render(tokens);
22/// assert_eq!(output, "\x1b[31mhello\x1b[0m");
23/// ```
24pub fn render(tokens: Vec<Token>) -> String {
25    let mut result = String::new();
26    for tok in tokens {
27        match tok {
28            Token::Text(text) => result.push_str(text.as_str()),
29            Token::Tag(tag) => match tag {
30                TagType::Color { color, ground } => {
31                    result.push_str(color_to_ansi(&color, ground).as_str())
32                }
33                TagType::Emphasis(emphasis) => {
34                    result.push_str(emphasis_to_ansi(&emphasis).as_str())
35                }
36                TagType::Reset => result.push_str("\x1b[0m"),
37                TagType::Prefix(prefix) => result.push_str(prefix.as_str()),
38            },
39        }
40    }
41
42    result
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use crate::ansi::{Color, Ground, NamedColor};
49    use crate::lexer::{EmphasisType, TagType, Token};
50
51    // --- render ---
52    #[test]
53    fn test_render_empty_token_list() {
54        let result = render(vec![]);
55        assert_eq!(result, "");
56    }
57    #[test]
58    fn test_render_plain_text_token() {
59        let result = render(vec![Token::Text("hello".into())]);
60        assert_eq!(result, "hello");
61    }
62    #[test]
63    fn test_render_named_color_tag() {
64        let result = render(vec![Token::Tag(TagType::Color {
65            color: Color::Named(NamedColor::Red),
66            ground: Ground::Foreground,
67        })]);
68        assert_eq!(result, "\x1b[31m");
69    }
70    #[test]
71    fn test_render_emphasis_tag_bold() {
72        let result = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
73        assert_eq!(result, "\x1b[1m");
74    }
75    #[test]
76    fn test_render_reset_tag() {
77        let result = render(vec![Token::Tag(TagType::Reset)]);
78        assert_eq!(result, "\x1b[0m");
79    }
80    #[test]
81    fn test_render_color_then_text() {
82        let result = render(vec![
83            Token::Tag(TagType::Color {
84                color: Color::Named(NamedColor::Red),
85                ground: Ground::Foreground,
86            }),
87            Token::Text("hello".into()),
88        ]);
89        assert_eq!(result, "\x1b[31mhello");
90    }
91    #[test]
92    fn test_render_color_text_reset() {
93        let result = render(vec![
94            Token::Tag(TagType::Color {
95                color: Color::Named(NamedColor::Green),
96                ground: Ground::Foreground,
97            }),
98            Token::Text("go".into()),
99            Token::Tag(TagType::Reset),
100        ]);
101        assert_eq!(result, "\x1b[32mgo\x1b[0m");
102    }
103    #[test]
104    fn test_render_multiple_text_tokens() {
105        let result = render(vec![Token::Text("foo".into()), Token::Text("bar".into())]);
106        assert_eq!(result, "foobar");
107    }
108    #[test]
109    fn test_render_ansi256_color_tag() {
110        let result = render(vec![Token::Tag(TagType::Color {
111            color: Color::Ansi256(21),
112            ground: Ground::Foreground,
113        })]);
114        assert_eq!(result, "\x1b[38;5;21m");
115    }
116    #[test]
117    fn test_render_rgb_color_tag() {
118        let result = render(vec![Token::Tag(TagType::Color {
119            color: Color::Rgb(255, 0, 0),
120            ground: Ground::Foreground,
121        })]);
122        assert_eq!(result, "\x1b[38;2;255;0;0m");
123    }
124    #[test]
125    fn test_render_does_not_append_trailing_reset() {
126        let result = render(vec![Token::Text("plain".into())]);
127        assert!(!result.ends_with("\x1b[0m"));
128    }
129    #[test]
130    fn test_render_named_color_background() {
131        let result = render(vec![Token::Tag(TagType::Color {
132            color: Color::Named(NamedColor::Red),
133            ground: Ground::Background,
134        })]);
135        assert_eq!(result, "\x1b[41m");
136    }
137    #[test]
138    fn test_render_ansi256_background() {
139        let result = render(vec![Token::Tag(TagType::Color {
140            color: Color::Ansi256(21),
141            ground: Ground::Background,
142        })]);
143        assert_eq!(result, "\x1b[48;5;21m");
144    }
145    #[test]
146    fn test_render_rgb_background() {
147        let result = render(vec![Token::Tag(TagType::Color {
148            color: Color::Rgb(255, 0, 0),
149            ground: Ground::Background,
150        })]);
151        assert_eq!(result, "\x1b[48;2;255;0;0m");
152    }
153    #[test]
154    fn test_render_fg_and_bg_together() {
155        let result = render(vec![
156            Token::Tag(TagType::Color {
157                color: Color::Named(NamedColor::White),
158                ground: Ground::Foreground,
159            }),
160            Token::Tag(TagType::Color {
161                color: Color::Named(NamedColor::Blue),
162                ground: Ground::Background,
163            }),
164            Token::Text("hello".into()),
165        ]);
166        assert_eq!(result, "\x1b[37m\x1b[44mhello");
167    }
168}
169// Skipped (side effects): none: render() is a pure function.