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    let mut active: Vec<TagType> = Vec::new();
27    for t in tokens {
28        match t {
29            Token::Text(s) | Token::Tag(TagType::Prefix(s)) => result.push_str(&s),
30            Token::Tag(TagType::Color { color, ground }) => {
31                result.push_str(&color_to_ansi(&color, ground.clone()));
32                active.push(TagType::Color { color, ground });
33            }
34            Token::Tag(TagType::Emphasis(e)) => {
35                result.push_str(&emphasis_to_ansi(&e));
36                active.push(TagType::Emphasis(e));
37            }
38            Token::Tag(TagType::Reset(None)) => {
39                result.push_str("\x1b[0m");
40                active.clear();
41            }
42            Token::Tag(TagType::Reset(Some(r))) => {
43                result.push_str("\x1b[0m");
44                active.retain(|x| x != r.as_ref());
45                for a in &active {
46                    match a {
47                        TagType::Color { color, ground } => {
48                            result.push_str(&color_to_ansi(color, ground.clone()))
49                        }
50                        TagType::Emphasis(e) => result.push_str(&emphasis_to_ansi(e)),
51                        _ => {}
52                    }
53                }
54            }
55        }
56    }
57    result
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::ansi::{Color, Ground, NamedColor};
64    use crate::lexer::{EmphasisType, TagType, Token};
65
66    // --- render ---
67    #[test]
68    fn test_render_empty_token_list() {
69        let result = render(vec![]);
70        assert_eq!(result, "");
71    }
72    #[test]
73    fn test_render_plain_text_token() {
74        let result = render(vec![Token::Text("hello".into())]);
75        assert_eq!(result, "hello");
76    }
77    #[test]
78    fn test_render_named_color_tag() {
79        let result = render(vec![Token::Tag(TagType::Color {
80            color: Color::Named(NamedColor::Red),
81            ground: Ground::Foreground,
82        })]);
83        assert_eq!(result, "\x1b[31m");
84    }
85    #[test]
86    fn test_render_emphasis_tag_bold() {
87        let result = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
88        assert_eq!(result, "\x1b[1m");
89    }
90    #[test]
91    fn test_render_reset_tag() {
92        let result = render(vec![Token::Tag(TagType::Reset(None))]);
93        assert_eq!(result, "\x1b[0m");
94    }
95    #[test]
96    fn test_render_color_then_text() {
97        let result = render(vec![
98            Token::Tag(TagType::Color {
99                color: Color::Named(NamedColor::Red),
100                ground: Ground::Foreground,
101            }),
102            Token::Text("hello".into()),
103        ]);
104        assert_eq!(result, "\x1b[31mhello");
105    }
106    #[test]
107    fn test_render_color_text_reset() {
108        let result = render(vec![
109            Token::Tag(TagType::Color {
110                color: Color::Named(NamedColor::Green),
111                ground: Ground::Foreground,
112            }),
113            Token::Text("go".into()),
114            Token::Tag(TagType::Reset(None)),
115        ]);
116        assert_eq!(result, "\x1b[32mgo\x1b[0m");
117    }
118    #[test]
119    fn test_render_multiple_text_tokens() {
120        let result = render(vec![Token::Text("foo".into()), Token::Text("bar".into())]);
121        assert_eq!(result, "foobar");
122    }
123    #[test]
124    fn test_render_ansi256_color_tag() {
125        let result = render(vec![Token::Tag(TagType::Color {
126            color: Color::Ansi256(21),
127            ground: Ground::Foreground,
128        })]);
129        assert_eq!(result, "\x1b[38;5;21m");
130    }
131    #[test]
132    fn test_render_rgb_color_tag() {
133        let result = render(vec![Token::Tag(TagType::Color {
134            color: Color::Rgb(255, 0, 0),
135            ground: Ground::Foreground,
136        })]);
137        assert_eq!(result, "\x1b[38;2;255;0;0m");
138    }
139    #[test]
140    fn test_render_does_not_append_trailing_reset() {
141        let result = render(vec![Token::Text("plain".into())]);
142        assert!(!result.ends_with("\x1b[0m"));
143    }
144    #[test]
145    fn test_render_named_color_background() {
146        let result = render(vec![Token::Tag(TagType::Color {
147            color: Color::Named(NamedColor::Red),
148            ground: Ground::Background,
149        })]);
150        assert_eq!(result, "\x1b[41m");
151    }
152    #[test]
153    fn test_render_ansi256_background() {
154        let result = render(vec![Token::Tag(TagType::Color {
155            color: Color::Ansi256(21),
156            ground: Ground::Background,
157        })]);
158        assert_eq!(result, "\x1b[48;5;21m");
159    }
160    #[test]
161    fn test_render_rgb_background() {
162        let result = render(vec![Token::Tag(TagType::Color {
163            color: Color::Rgb(255, 0, 0),
164            ground: Ground::Background,
165        })]);
166        assert_eq!(result, "\x1b[48;2;255;0;0m");
167    }
168    #[test]
169    fn test_render_fg_and_bg_together() {
170        let result = render(vec![
171            Token::Tag(TagType::Color {
172                color: Color::Named(NamedColor::White),
173                ground: Ground::Foreground,
174            }),
175            Token::Tag(TagType::Color {
176                color: Color::Named(NamedColor::Blue),
177                ground: Ground::Background,
178            }),
179            Token::Text("hello".into()),
180        ]);
181        assert_eq!(result, "\x1b[37m\x1b[44mhello");
182    }
183}
184// Skipped (side effects): none: render() is a pure function.