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