1use crate::{
9 ansi::{color_to_ansi, emphasis_to_ansi},
10 env::color_enabled,
11 lexer::{TagType, Token},
12};
13
14pub 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) => Some(s.into_owned()),
33 Token::Tag(TagType::Prefix(s)) => Some(s),
34 _ => None,
35 })
36 .collect();
37 }
38 let mut result = String::with_capacity(tokens.len() * 16);
39 let mut active: Vec<TagType> = Vec::new();
40 for t in tokens {
41 match t {
42 Token::Text(s) => result.push_str(&s),
43 Token::Tag(TagType::Prefix(s)) => result.push_str(&s),
44 Token::Tag(TagType::Color { color, ground }) => {
45 #[cfg(feature = "lossy")]
46 let color = crate::degrader::degrade(color);
47 result.push_str(&color_to_ansi(&color, ground.clone()));
48 active.push(TagType::Color { color, ground });
49 }
50 Token::Tag(TagType::Emphasis(e)) => {
51 result.push_str(&emphasis_to_ansi(&e));
52 active.push(TagType::Emphasis(e));
53 }
54 Token::Tag(TagType::ResetAll) => {
55 result.push_str("\x1b[0m");
56 active.clear();
57 }
58 Token::Tag(TagType::ResetOne(r)) => {
59 result.push_str("\x1b[0m");
60 active.retain(|x| x != r.as_ref());
61 for a in &active {
62 match a {
63 TagType::Color { color, ground } => {
64 result.push_str(&color_to_ansi(color, ground.clone()))
65 }
66 TagType::Emphasis(e) => result.push_str(&emphasis_to_ansi(e)),
67 _ => {}
68 }
69 }
70 }
71 }
72 }
73 result
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::ansi::{Color, Ground, NamedColor};
80 use crate::env::color_enabled;
81 use crate::lexer::{EmphasisType, TagType, Token};
82
83 #[test]
85 fn test_render_empty_token_list() {
86 let result = render(vec![]);
87 assert_eq!(result, "");
88 }
89 #[test]
90 fn test_render_plain_text_token() {
91 let result = render(vec![Token::Text("hello".into())]);
92 assert_eq!(result, "hello");
93 }
94 #[test]
95 fn test_render_named_color_tag() {
96 if !color_enabled() { return; }
97 let result = render(vec![Token::Tag(TagType::Color {
98 color: Color::Named(NamedColor::Red),
99 ground: Ground::Foreground,
100 })]);
101 assert_eq!(result, "\x1b[31m");
102 }
103 #[test]
104 fn test_render_emphasis_tag_bold() {
105 if !color_enabled() { return; }
106 let result = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
107 assert_eq!(result, "\x1b[1m");
108 }
109 #[test]
110 fn test_render_reset_tag() {
111 if !color_enabled() { return; }
112 let result = render(vec![Token::Tag(TagType::ResetAll)]);
113 assert_eq!(result, "\x1b[0m");
114 }
115 #[test]
116 fn test_render_color_then_text() {
117 if !color_enabled() { return; }
118 let result = render(vec![
119 Token::Tag(TagType::Color {
120 color: Color::Named(NamedColor::Red),
121 ground: Ground::Foreground,
122 }),
123 Token::Text("hello".into()),
124 ]);
125 assert_eq!(result, "\x1b[31mhello");
126 }
127 #[test]
128 fn test_render_color_text_reset() {
129 if !color_enabled() { return; }
130 let result = render(vec![
131 Token::Tag(TagType::Color {
132 color: Color::Named(NamedColor::Green),
133 ground: Ground::Foreground,
134 }),
135 Token::Text("go".into()),
136 Token::Tag(TagType::ResetAll),
137 ]);
138 assert_eq!(result, "\x1b[32mgo\x1b[0m");
139 }
140 #[test]
141 fn test_render_multiple_text_tokens() {
142 let result = render(vec![Token::Text("foo".into()), Token::Text("bar".into())]);
143 assert_eq!(result, "foobar");
144 }
145 #[test]
146 fn test_render_ansi256_color_tag() {
147 if !color_enabled() { return; }
148 let result = render(vec![Token::Tag(TagType::Color {
149 color: Color::Ansi256(21),
150 ground: Ground::Foreground,
151 })]);
152 assert_eq!(result, "\x1b[38;5;21m");
153 }
154 #[test]
155 fn test_render_rgb_color_tag() {
156 if !color_enabled() { return; }
157 let result = render(vec![Token::Tag(TagType::Color {
158 color: Color::Rgb(255, 0, 0),
159 ground: Ground::Foreground,
160 })]);
161 assert_eq!(result, "\x1b[38;2;255;0;0m");
162 }
163 #[test]
164 fn test_render_does_not_append_trailing_reset() {
165 let result = render(vec![Token::Text("plain".into())]);
166 assert!(!result.ends_with("\x1b[0m"));
167 }
168 #[test]
169 fn test_render_named_color_background() {
170 if !color_enabled() { return; }
171 let result = render(vec![Token::Tag(TagType::Color {
172 color: Color::Named(NamedColor::Red),
173 ground: Ground::Background,
174 })]);
175 assert_eq!(result, "\x1b[41m");
176 }
177 #[test]
178 fn test_render_ansi256_background() {
179 if !color_enabled() { return; }
180 let result = render(vec![Token::Tag(TagType::Color {
181 color: Color::Ansi256(21),
182 ground: Ground::Background,
183 })]);
184 assert_eq!(result, "\x1b[48;5;21m");
185 }
186 #[test]
187 fn test_render_rgb_background() {
188 if !color_enabled() { return; }
189 let result = render(vec![Token::Tag(TagType::Color {
190 color: Color::Rgb(255, 0, 0),
191 ground: Ground::Background,
192 })]);
193 assert_eq!(result, "\x1b[48;2;255;0;0m");
194 }
195 #[test]
196 fn test_render_fg_and_bg_together() {
197 if !color_enabled() { return; }
198 let result = render(vec![
199 Token::Tag(TagType::Color {
200 color: Color::Named(NamedColor::White),
201 ground: Ground::Foreground,
202 }),
203 Token::Tag(TagType::Color {
204 color: Color::Named(NamedColor::Blue),
205 ground: Ground::Background,
206 }),
207 Token::Text("hello".into()),
208 ]);
209 assert_eq!(result, "\x1b[37m\x1b[44mhello");
210 }
211
212 #[test]
215 fn test_render_no_color_strips_tag_tokens() {
216 if color_enabled() { return; }
217 let result = render(vec![
218 Token::Tag(TagType::Color {
219 color: Color::Named(NamedColor::Red),
220 ground: Ground::Foreground,
221 }),
222 Token::Text("hello".into()),
223 Token::Tag(TagType::ResetAll),
224 ]);
225 assert_eq!(result, "hello");
226 }
227 #[test]
228 fn test_render_no_color_preserves_text_and_prefix() {
229 if color_enabled() { return; }
230 let result = render(vec![
231 Token::Tag(TagType::Prefix(">>".to_string())),
232 Token::Text(" world".into()),
233 ]);
234 assert_eq!(result, ">> world");
235 }
236 #[test]
237 fn test_render_no_color_pure_tags_produce_empty_string() {
238 if color_enabled() { return; }
239 let result = render(vec![
240 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
241 Token::Tag(TagType::ResetAll),
242 ]);
243 assert_eq!(result, "");
244 }
245 #[test]
246 fn test_render_no_color_reset_one_stripped() {
247 if color_enabled() { return; }
248 let result = render(vec![
249 Token::Tag(TagType::ResetOne(Box::new(TagType::Emphasis(
250 EmphasisType::Bold,
251 )))),
252 Token::Text("plain".into()),
253 ]);
254 assert_eq!(result, "plain");
255 }
256}
257