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