1use crate::{
9 ansi::{color_to_ansi, emphasis_to_ansi},
10 env::color_enabled,
11 lexer::{TagType, Token},
12 state::{active_stack, set_active_stack},
13};
14
15pub fn render(tokens: Vec<Token>) -> String {
29 if !color_enabled() {
30 return tokens
31 .into_iter()
32 .filter_map(|t| match t {
33 Token::Text(s) => Some(s.into_owned()),
34 Token::Tag(TagType::Prefix(s)) => Some(s),
35 _ => None,
36 })
37 .collect();
38 }
39 let mut result = String::with_capacity(tokens.len() * 16);
40 let mut active: Vec<TagType> = active_stack();
41 for t in tokens {
42 match t {
43 Token::Text(s) => result.push_str(&s),
44 Token::Tag(TagType::Prefix(s)) => result.push_str(&s),
45 Token::Tag(TagType::Color { color, ground }) => {
46 #[cfg(feature = "lossy")]
47 let color = crate::degrader::degrade(color);
48 result.push_str(&color_to_ansi(&color, ground.clone()));
49 active.push(TagType::Color { color, ground });
50 }
51 Token::Tag(TagType::Emphasis(e)) => {
52 result.push_str(&emphasis_to_ansi(&e));
53 active.push(TagType::Emphasis(e));
54 }
55 Token::Tag(TagType::ResetAll) => {
56 result.push_str("\x1b[0m");
57 active.clear();
58 }
59 Token::Tag(TagType::ResetOne(r)) => {
60 result.push_str("\x1b[0m");
61 active.retain(|x| x != r.as_ref());
62 for a in &active {
63 match a {
64 TagType::Color { color, ground } => {
65 result.push_str(&color_to_ansi(color, ground.clone()))
66 }
67 TagType::Emphasis(e) => result.push_str(&emphasis_to_ansi(e)),
68 _ => {}
69 }
70 }
71 }
72 }
73 }
74 set_active_stack(active);
75 result
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::ansi::{Color, Ground, NamedColor};
82 use crate::env::color_enabled;
83 use crate::lexer::{EmphasisType, TagType, Token};
84
85 #[test]
87 fn test_render_empty_token_list() {
88 let result = render(vec![]);
89 assert_eq!(result, "");
90 }
91 #[test]
92 fn test_render_plain_text_token() {
93 let result = render(vec![Token::Text("hello".into())]);
94 assert_eq!(result, "hello");
95 }
96 #[test]
97 fn test_render_named_color_tag() {
98 if !color_enabled() {
99 return;
100 }
101 let result = render(vec![Token::Tag(TagType::Color {
102 color: Color::Named(NamedColor::Red),
103 ground: Ground::Foreground,
104 })]);
105 assert_eq!(result, "\x1b[31m");
106 }
107 #[test]
108 fn test_render_emphasis_tag_bold() {
109 if !color_enabled() {
110 return;
111 }
112 let result = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
113 assert_eq!(result, "\x1b[1m");
114 }
115 #[test]
116 fn test_render_reset_tag() {
117 if !color_enabled() {
118 return;
119 }
120 let result = render(vec![Token::Tag(TagType::ResetAll)]);
121 assert_eq!(result, "\x1b[0m");
122 }
123 #[test]
124 fn test_render_color_then_text() {
125 if !color_enabled() {
126 return;
127 }
128 let result = render(vec![
129 Token::Tag(TagType::Color {
130 color: Color::Named(NamedColor::Red),
131 ground: Ground::Foreground,
132 }),
133 Token::Text("hello".into()),
134 ]);
135 assert_eq!(result, "\x1b[31mhello");
136 }
137 #[test]
138 fn test_render_color_text_reset() {
139 if !color_enabled() {
140 return;
141 }
142 let result = render(vec![
143 Token::Tag(TagType::Color {
144 color: Color::Named(NamedColor::Green),
145 ground: Ground::Foreground,
146 }),
147 Token::Text("go".into()),
148 Token::Tag(TagType::ResetAll),
149 ]);
150 assert_eq!(result, "\x1b[32mgo\x1b[0m");
151 }
152 #[test]
153 fn test_render_multiple_text_tokens() {
154 let result = render(vec![Token::Text("foo".into()), Token::Text("bar".into())]);
155 assert_eq!(result, "foobar");
156 }
157 #[test]
158 fn test_render_ansi256_color_tag() {
159 if !color_enabled() {
160 return;
161 }
162 let result = render(vec![Token::Tag(TagType::Color {
163 color: Color::Ansi256(21),
164 ground: Ground::Foreground,
165 })]);
166 assert_eq!(result, "\x1b[38;5;21m");
167 }
168 #[test]
169 fn test_render_rgb_color_tag() {
170 if !color_enabled() {
171 return;
172 }
173 let result = render(vec![Token::Tag(TagType::Color {
174 color: Color::Rgb(255, 0, 0),
175 ground: Ground::Foreground,
176 })]);
177 assert_eq!(result, "\x1b[38;2;255;0;0m");
178 }
179 #[test]
180 fn test_render_does_not_append_trailing_reset() {
181 let result = render(vec![Token::Text("plain".into())]);
182 assert!(!result.ends_with("\x1b[0m"));
183 }
184 #[test]
185 fn test_render_named_color_background() {
186 if !color_enabled() {
187 return;
188 }
189 let result = render(vec![Token::Tag(TagType::Color {
190 color: Color::Named(NamedColor::Red),
191 ground: Ground::Background,
192 })]);
193 assert_eq!(result, "\x1b[41m");
194 }
195 #[test]
196 fn test_render_ansi256_background() {
197 if !color_enabled() {
198 return;
199 }
200 let result = render(vec![Token::Tag(TagType::Color {
201 color: Color::Ansi256(21),
202 ground: Ground::Background,
203 })]);
204 assert_eq!(result, "\x1b[48;5;21m");
205 }
206 #[test]
207 fn test_render_rgb_background() {
208 if !color_enabled() {
209 return;
210 }
211 let result = render(vec![Token::Tag(TagType::Color {
212 color: Color::Rgb(255, 0, 0),
213 ground: Ground::Background,
214 })]);
215 assert_eq!(result, "\x1b[48;2;255;0;0m");
216 }
217 #[test]
218 fn test_render_fg_and_bg_together() {
219 if !color_enabled() {
220 return;
221 }
222 let result = render(vec![
223 Token::Tag(TagType::Color {
224 color: Color::Named(NamedColor::White),
225 ground: Ground::Foreground,
226 }),
227 Token::Tag(TagType::Color {
228 color: Color::Named(NamedColor::Blue),
229 ground: Ground::Background,
230 }),
231 Token::Text("hello".into()),
232 ]);
233 assert_eq!(result, "\x1b[37m\x1b[44mhello");
234 }
235
236 #[test]
239 fn test_render_no_color_strips_tag_tokens() {
240 if color_enabled() {
241 return;
242 }
243 let result = render(vec![
244 Token::Tag(TagType::Color {
245 color: Color::Named(NamedColor::Red),
246 ground: Ground::Foreground,
247 }),
248 Token::Text("hello".into()),
249 Token::Tag(TagType::ResetAll),
250 ]);
251 assert_eq!(result, "hello");
252 }
253 #[test]
254 fn test_render_no_color_preserves_text_and_prefix() {
255 if color_enabled() {
256 return;
257 }
258 let result = render(vec![
259 Token::Tag(TagType::Prefix(">>".to_string())),
260 Token::Text(" world".into()),
261 ]);
262 assert_eq!(result, ">> world");
263 }
264 #[test]
265 fn test_render_no_color_pure_tags_produce_empty_string() {
266 if color_enabled() {
267 return;
268 }
269 let result = render(vec![
270 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
271 Token::Tag(TagType::ResetAll),
272 ]);
273 assert_eq!(result, "");
274 }
275 #[test]
276 fn test_render_no_color_reset_one_stripped() {
277 if color_enabled() {
278 return;
279 }
280 let result = render(vec![
281 Token::Tag(TagType::ResetOne(Box::new(TagType::Emphasis(
282 EmphasisType::Bold,
283 )))),
284 Token::Text("plain".into()),
285 ]);
286 assert_eq!(result, "plain");
287 }
288 #[test]
289 fn test_render_resumes_persisted_stack() {
290 if !color_enabled() {
291 return;
292 }
293 crate::clear_active_stack();
294
295 let _ = render(vec![
296 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
297 Token::Tag(TagType::Color {
298 color: Color::Named(NamedColor::Red),
299 ground: Ground::Foreground,
300 }),
301 ]);
302
303 let result = render(vec![
304 Token::Tag(TagType::ResetOne(Box::new(TagType::Color {
305 color: Color::Named(NamedColor::Red),
306 ground: Ground::Foreground,
307 }))),
308 Token::Text("ok".into()),
309 ]);
310 assert_eq!(result, "\x1b[0m\x1b[1mok");
311
312 crate::clear_active_stack();
313 }
314
315 #[test]
316 fn test_render_persists_active_stack() {
317 if !color_enabled() {
318 return;
319 }
320 crate::clear_active_stack();
321
322 let _ = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
323 assert_eq!(
324 crate::active_stack(),
325 vec![TagType::Emphasis(EmphasisType::Bold)]
326 );
327
328 crate::clear_active_stack();
329 }
330
331 #[test]
332 fn test_render_reset_all_clears_persisted_stack() {
333 if !color_enabled() {
334 return;
335 }
336 crate::clear_active_stack();
337
338 let _ = render(vec![
339 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
340 Token::Tag(TagType::ResetAll),
341 ]);
342 assert!(crate::active_stack().is_empty());
343 }
344}
345