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