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