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