1use crate::{
9 ansi::{write_color_ansi, write_emphasis_ansi},
10 env::color_enabled,
11 errors::LexError,
12 lexer::{TagType, Token, parse_part, split_tag_parts},
13 state::{active_stack, set_active_stack},
14 strip::strip_markup,
15};
16
17fn apply_tag(tag: TagType, result: &mut String, active: &mut Vec<TagType>) {
20 match tag {
21 TagType::Prefix(s) => result.push_str(&s),
22 TagType::Color { color, ground } => {
23 #[cfg(feature = "lossy")]
24 let color = crate::degrader::degrade(color);
25 write_color_ansi(result, &color, ground);
26 active.push(TagType::Color { color, ground });
27 }
28 TagType::Emphasis(e) => {
29 write_emphasis_ansi(result, &e);
30 active.push(TagType::Emphasis(e));
31 }
32 TagType::ResetAll => {
33 result.push_str("\x1b[0m");
34 active.clear();
35 }
36 TagType::ResetOne(r) => {
37 result.push_str("\x1b[0m");
38 active.retain(|x| !r.matches_tag(x));
39 for a in &*active {
40 match a {
41 TagType::Color { color, ground } => {
42 write_color_ansi(result, color, *ground);
43 }
44 TagType::Emphasis(e) => write_emphasis_ansi(result, e),
45 _ => {}
46 }
47 }
48 }
49 }
50}
51
52#[must_use]
66pub fn render(tokens: Vec<Token>) -> String {
67 if !color_enabled() {
68 return tokens
69 .into_iter()
70 .filter_map(|t| match t {
71 Token::Text(s) => Some(s.into_owned()),
72 Token::Tag(TagType::Prefix(s)) => Some(s),
73 Token::Tag(_) => None,
74 })
75 .collect();
76 }
77 render_forced(tokens)
78}
79
80#[must_use]
84pub fn render_forced(tokens: Vec<Token>) -> String {
85 let mut result = String::with_capacity(tokens.len() * 16);
86 let mut active: Vec<TagType> = active_stack();
87 for t in tokens {
88 match t {
89 Token::Text(s) => result.push_str(&s),
90 Token::Tag(tag) => apply_tag(tag, &mut result, &mut active),
91 }
92 }
93 set_active_stack(active);
94 result
95}
96
97pub fn render_str(input: &str) -> Result<String, LexError> {
109 if !color_enabled() {
110 crate::lexer::tokenize(input)?; return Ok(strip_markup(input));
112 }
113 render_forced_str(input)
114}
115
116fn render_forced_str(input: &str) -> Result<String, LexError> {
118 let mut result = String::with_capacity(input.len() + input.len() / 4);
119 let mut active: Vec<TagType> = active_stack();
120 let mut tag_types = Vec::new();
121 let bytes = input.as_bytes();
122 let mut pos = 0;
123
124 while pos < input.len() {
125 let next = {
127 let rest = &input[pos..];
128 let open = rest.find('[');
129 let close = rest.find(']');
130 match (open, close) {
131 (Some(o), Some(c)) if o <= c => Some((pos + o, b'[')),
132 (Some(_) | None, Some(c)) => Some((pos + c, b']')),
133 (Some(o), None) => Some((pos + o, b'[')),
134 (None, None) => None,
135 }
136 };
137
138 let Some((abs_pos, kind)) = next else {
139 if pos < input.len() {
141 result.push_str(&input[pos..]);
142 }
143 break;
144 };
145
146 if abs_pos > pos {
148 result.push_str(&input[pos..abs_pos]);
149 }
150
151 match kind {
152 b']' => {
153 if abs_pos + 1 < input.len() && bytes[abs_pos + 1] == b']' {
154 result.push(']');
155 pos = abs_pos + 2;
156 } else {
157 result.push(']');
158 pos = abs_pos + 1;
159 }
160 }
161 b'[' => {
162 if abs_pos > 0 && bytes[abs_pos - 1] == b'\x1b' {
164 result.push_str("\x1b[");
165 pos = abs_pos + 1;
166 continue;
167 }
168
169 if abs_pos + 1 < input.len() && bytes[abs_pos + 1] == b'[' {
171 result.push('[');
172 pos = abs_pos + 2;
173 continue;
174 }
175
176 let tag_start = abs_pos + 1;
178 let closing = input[tag_start..]
179 .find(']')
180 .ok_or(LexError::UnclosedTag(abs_pos))?;
181 let raw_tag = &input[tag_start..tag_start + closing];
182
183 tag_types.clear();
185 for (offset, part) in split_tag_parts(raw_tag) {
186 let abs_off = tag_start + offset;
187 parse_part(part, abs_off, &mut tag_types)?;
188 }
189 for t in tag_types.drain(..) {
190 apply_tag(t, &mut result, &mut active);
191 }
192
193 pos = tag_start + closing + 1;
194 }
195 _ => unreachable!(),
196 }
197 }
198
199 set_active_stack(active);
200 Ok(result)
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::ansi::{Color, Ground, NamedColor};
207 use crate::env::color_enabled;
208 use crate::lexer::{EmphasisType, ResetKind, TagType, Token};
209
210 #[test]
212 fn test_render_empty_token_list() {
213 let result = render(vec![]);
214 assert_eq!(result, "");
215 }
216 #[test]
217 fn test_render_plain_text_token() {
218 let result = render(vec![Token::Text("hello".into())]);
219 assert_eq!(result, "hello");
220 }
221 #[test]
222 fn test_render_named_color_tag() {
223 if !color_enabled() {
224 return;
225 }
226 let result = render(vec![Token::Tag(TagType::Color {
227 color: Color::Named(NamedColor::Red),
228 ground: Ground::Foreground,
229 })]);
230 assert_eq!(result, "\x1b[31m");
231 }
232 #[test]
233 fn test_render_emphasis_tag_bold() {
234 if !color_enabled() {
235 return;
236 }
237 let result = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
238 assert_eq!(result, "\x1b[1m");
239 }
240 #[test]
241 fn test_render_reset_tag() {
242 if !color_enabled() {
243 return;
244 }
245 let result = render(vec![Token::Tag(TagType::ResetAll)]);
246 assert_eq!(result, "\x1b[0m");
247 }
248 #[test]
249 fn test_render_color_then_text() {
250 if !color_enabled() {
251 return;
252 }
253 let result = render(vec![
254 Token::Tag(TagType::Color {
255 color: Color::Named(NamedColor::Red),
256 ground: Ground::Foreground,
257 }),
258 Token::Text("hello".into()),
259 ]);
260 assert_eq!(result, "\x1b[31mhello");
261 }
262 #[test]
263 fn test_render_color_text_reset() {
264 if !color_enabled() {
265 return;
266 }
267 let result = render(vec![
268 Token::Tag(TagType::Color {
269 color: Color::Named(NamedColor::Green),
270 ground: Ground::Foreground,
271 }),
272 Token::Text("go".into()),
273 Token::Tag(TagType::ResetAll),
274 ]);
275 assert_eq!(result, "\x1b[32mgo\x1b[0m");
276 }
277 #[test]
278 fn test_render_multiple_text_tokens() {
279 let result = render(vec![Token::Text("foo".into()), Token::Text("bar".into())]);
280 assert_eq!(result, "foobar");
281 }
282 #[test]
283 fn test_render_ansi256_color_tag() {
284 if !color_enabled() {
285 return;
286 }
287 let result = render(vec![Token::Tag(TagType::Color {
288 color: Color::Ansi256(21),
289 ground: Ground::Foreground,
290 })]);
291 assert_eq!(result, "\x1b[38;5;21m");
292 }
293 #[test]
294 fn test_render_rgb_color_tag() {
295 if !color_enabled() {
296 return;
297 }
298 let result = render(vec![Token::Tag(TagType::Color {
299 color: Color::Rgb(255, 0, 0),
300 ground: Ground::Foreground,
301 })]);
302 assert_eq!(result, "\x1b[38;2;255;0;0m");
303 }
304 #[test]
305 fn test_render_does_not_append_trailing_reset() {
306 let result = render(vec![Token::Text("plain".into())]);
307 assert!(!result.ends_with("\x1b[0m"));
308 }
309 #[test]
310 fn test_render_named_color_background() {
311 if !color_enabled() {
312 return;
313 }
314 let result = render(vec![Token::Tag(TagType::Color {
315 color: Color::Named(NamedColor::Red),
316 ground: Ground::Background,
317 })]);
318 assert_eq!(result, "\x1b[41m");
319 }
320 #[test]
321 fn test_render_ansi256_background() {
322 if !color_enabled() {
323 return;
324 }
325 let result = render(vec![Token::Tag(TagType::Color {
326 color: Color::Ansi256(21),
327 ground: Ground::Background,
328 })]);
329 assert_eq!(result, "\x1b[48;5;21m");
330 }
331 #[test]
332 fn test_render_rgb_background() {
333 if !color_enabled() {
334 return;
335 }
336 let result = render(vec![Token::Tag(TagType::Color {
337 color: Color::Rgb(255, 0, 0),
338 ground: Ground::Background,
339 })]);
340 assert_eq!(result, "\x1b[48;2;255;0;0m");
341 }
342 #[test]
343 fn test_render_fg_and_bg_together() {
344 if !color_enabled() {
345 return;
346 }
347 let result = render(vec![
348 Token::Tag(TagType::Color {
349 color: Color::Named(NamedColor::White),
350 ground: Ground::Foreground,
351 }),
352 Token::Tag(TagType::Color {
353 color: Color::Named(NamedColor::Blue),
354 ground: Ground::Background,
355 }),
356 Token::Text("hello".into()),
357 ]);
358 assert_eq!(result, "\x1b[37m\x1b[44mhello");
359 }
360
361 #[test]
364 fn test_render_no_color_strips_tag_tokens() {
365 if color_enabled() {
366 return;
367 }
368 let result = render(vec![
369 Token::Tag(TagType::Color {
370 color: Color::Named(NamedColor::Red),
371 ground: Ground::Foreground,
372 }),
373 Token::Text("hello".into()),
374 Token::Tag(TagType::ResetAll),
375 ]);
376 assert_eq!(result, "hello");
377 }
378 #[test]
379 fn test_render_no_color_preserves_text_and_prefix() {
380 if color_enabled() {
381 return;
382 }
383 let result = render(vec![
384 Token::Tag(TagType::Prefix(">>".to_string())),
385 Token::Text(" world".into()),
386 ]);
387 assert_eq!(result, ">> world");
388 }
389 #[test]
390 fn test_render_no_color_pure_tags_produce_empty_string() {
391 if color_enabled() {
392 return;
393 }
394 let result = render(vec![
395 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
396 Token::Tag(TagType::ResetAll),
397 ]);
398 assert_eq!(result, "");
399 }
400 #[test]
401 fn test_render_no_color_reset_one_stripped() {
402 if color_enabled() {
403 return;
404 }
405 let result = render(vec![
406 Token::Tag(TagType::ResetOne(ResetKind::Emphasis(EmphasisType::Bold))),
407 Token::Text("plain".into()),
408 ]);
409 assert_eq!(result, "plain");
410 }
411 #[test]
412 fn test_render_resumes_persisted_stack() {
413 if !color_enabled() {
414 return;
415 }
416 crate::clear_active_stack();
417
418 let _ = render(vec![
419 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
420 Token::Tag(TagType::Color {
421 color: Color::Named(NamedColor::Red),
422 ground: Ground::Foreground,
423 }),
424 ]);
425
426 let result = render(vec![
427 Token::Tag(TagType::ResetOne(ResetKind::Color {
428 color: Color::Named(NamedColor::Red),
429 ground: Ground::Foreground,
430 })),
431 Token::Text("ok".into()),
432 ]);
433 assert_eq!(result, "\x1b[0m\x1b[1mok");
434
435 crate::clear_active_stack();
436 }
437
438 #[test]
439 fn test_render_persists_active_stack() {
440 if !color_enabled() {
441 return;
442 }
443 crate::clear_active_stack();
444
445 let _ = render(vec![Token::Tag(TagType::Emphasis(EmphasisType::Bold))]);
446 assert_eq!(
447 crate::active_stack(),
448 vec![TagType::Emphasis(EmphasisType::Bold)]
449 );
450
451 crate::clear_active_stack();
452 }
453
454 #[test]
455 fn test_render_reset_all_clears_persisted_stack() {
456 if !color_enabled() {
457 return;
458 }
459 crate::clear_active_stack();
460
461 let _ = render(vec![
462 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
463 Token::Tag(TagType::ResetAll),
464 ]);
465 assert!(crate::active_stack().is_empty());
466 }
467}
468