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