banner/
lib.rs

1mod content;
2mod rendering;
3mod style;
4
5use content::{KeyValueLine, Line, TextLine};
6use rendering::BorderPainter;
7pub use style::{Color, HeaderLevel, Style};
8
9pub struct Banner<'a> {
10    pub width: u8,
11    pub auto_widen: bool,
12    style: &'a Style,
13    lines: Vec<Box<dyn Line + 'a>>,
14}
15
16impl<'a> Banner<'a> {
17    /// Creates a new banner with default values.
18    pub fn new(style: &'a Style) -> Banner<'a> {
19        return Banner {
20            width: 50,
21            auto_widen: true,
22            style: style,
23            lines: Vec::new(),
24        };
25    }
26
27    /// Adds a header to the banner.
28    ///
29    /// # Arguments
30    ///
31    /// * `self` - The banner to add the line of text to.
32    /// * `text` - The text content of the header.
33    /// * `level` - The header level.
34    pub fn add_header<'b>(&'b mut self, text: &'a str, level: HeaderLevel) {
35        let line = TextLine::new(text, &self.style.header_style(&level));
36
37        // Check if banner needs to be widened
38        let line_width = line.width();
39        if self.auto_widen && line_width > self.width {
40            self.width = line_width
41        }
42
43        self.lines.push(Box::new(line));
44    }
45
46    /// Adds a line of text to the banner.
47    ///
48    /// # Arguments
49    ///
50    /// * `self` - The banner to add the line of text to.
51    /// * `text` - The text to add.
52    pub fn add_text<'b>(&'b mut self, text: &'a str) {
53        let line = TextLine::new(text, &self.style.text);
54
55        // Check if banner needs to be widened
56        let line_width = line.width();
57        if self.auto_widen && line_width > self.width {
58            self.width = line_width
59        }
60
61        self.lines.push(Box::new(line));
62    }
63
64    /// Adds a line showing a key value pair to the banner.
65    ///
66    /// # Arguments
67    ///
68    /// * `self` - The banner to add the line to.
69    /// * `key` - The key name.
70    /// * `value` - The value as text.
71    pub fn add_key_value<'b>(&'b mut self, key: &'a str, value: &'a str) {
72        let line = KeyValueLine::new(key, value, &self.style.text);
73
74        // Check if banner needs to be widened
75        let line_width = line.width();
76        if self.auto_widen && line_width > self.width {
77            self.width = line_width
78        }
79
80        self.lines.push(Box::new(line));
81    }
82
83    /// Assembles the banner.
84    ///
85    /// # Arguments
86    ///
87    /// * `self` - The banner to assemble.
88    pub fn assemble(self: &Banner<'a>) -> String {
89        let border_painter: BorderPainter =
90            BorderPainter::new(&self.style.border, self.style.no_color_codes, self.width);
91
92        let mut result: String;
93        result = format!("{}\r\n", border_painter.top());
94        for line in self.lines.iter() {
95            let line_text = &(*line).fmt(self.style.no_color_codes);
96            let line_width = (*line).width();
97            // Add left border
98            result.push_str(&border_painter.left());
99            // Add line content
100            result.push_str(&format!("{}", line_text)[..]);
101            // Add whitespace to end
102            result.push_str(&format!(
103                "{}",
104                (line_width as usize..self.width as usize)
105                    .map(|_| " ")
106                    .collect::<String>()
107            ));
108            // Add right border
109            result.push_str(&border_painter.right());
110            result.push_str("\r\n");
111        }
112        result.push_str(&format!("{}\r\n", border_painter.bottom())[..]);
113
114        result
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::style::Color;
121    use super::*;
122
123    // #region Tests for color code suppression
124
125    /// Tests that an banner can be rendered without color codes.
126    #[test]
127    fn test_assemble_empty_no_color_codes() {
128        // Create a style with suppressed color codes
129        let mut style: Style = Style::new();
130        style.no_color_codes = true;
131
132        let mut banner: Banner = Banner::new(&style);
133        banner.width = 4;
134        assert_eq!("┌────┐\r\n└────┘\r\n", banner.assemble());
135    }
136
137    /// Tests that an banner can be rendered without color codes.
138    #[test]
139    fn test_assemble_right_padding() {
140        // Create a style with suppressed color codes
141        let mut style: Style = Style::new();
142        style.no_color_codes = true;
143
144        let mut banner: Banner = Banner::new(&style);
145        banner.width = 10;
146        banner.add_text("Test");
147
148        assert_eq!("┌──────────┐\r\n│Test      │\r\n└──────────┘\r\n", banner.assemble());
149    }
150
151    /// Tests that an banner can be rendered without color codes.
152    #[test]
153    fn test_assemble_auto_width() {
154        // Create a style with suppressed color codes
155        let mut style: Style = Style::new();
156        style.no_color_codes = true;
157
158        let mut banner: Banner = Banner::new(&style);
159        banner.width = 2; // Banner declared with width 2
160        banner.add_text("Test"); // but should auto-widen to 4
161
162        assert_eq!("┌────┐\r\n│Test│\r\n└────┘\r\n", banner.assemble());
163    }
164
165    #[test]
166    fn test_assemble_banner_no_color_codes() {
167        // Create a style with suppressed color codes
168        let mut style: Style = Style::new();
169        style.no_color_codes = true;
170
171        let mut banner: Banner = Banner::new(&style);
172        banner.width = 12;
173        banner.add_header("Header h1", HeaderLevel::H1);
174        banner.add_header("Header h2", HeaderLevel::H2);
175        banner.add_header("Header h3", HeaderLevel::H3);
176        banner.add_text("Text");
177        banner.add_key_value("Key", "Val");
178
179        let expected = "┌────────────┐\r\n│Header h1   │\r\n│Header h2   │\r\n│Header h3   │\r\n│Text        │\r\n│Key: Val    │\r\n└────────────┘\r\n";
180        assert_eq!(expected, banner.assemble());
181    }
182
183    // #endregion
184
185    /// Tests that an empty banner is assembled correctly.
186    #[test]
187    fn test_assemble_empty() {
188        // Create a style
189        let mut style: Style = Style::new();
190        style.border.color = Color::White;
191
192        // Build the banner
193        let mut banner: Banner = Banner::new(&style);
194        banner.width = 4;
195
196        let expected = "\u{1b}[37m┌────┐\u{1b}[0m\r\n\u{1b}[37m└────┘\u{1b}[0m\r\n";
197        assert_eq!(expected, banner.assemble());
198    }
199
200    /// Verifies that a banner with a single text line is assembled correctly.
201    #[test]
202    fn test_assemble_simple() {
203        // Create a style
204        let mut style: Style = Style::new();
205        style.no_color_codes = true;
206
207        // Build the banner
208        let mut banner: Banner = Banner::new(&style);
209        banner.width = 16;
210        banner.add_text("Hello!");
211
212        let expected = "┌────────────────┐\r\n│Hello!          │\r\n└────────────────┘\r\n";
213        assert_eq!(expected, banner.assemble());
214    }
215
216    /// Verifies that a banner with a single text line is assembled correctly.
217    #[test]
218    fn test_assemble_simple_colored() {
219        // Create a style
220        let mut style: Style = Style::new();
221        style.border.color = Color::White;
222        style.text.content_color = Color::Red;
223
224        // Build a banner
225        let mut banner: Banner = Banner::new(&style);
226        banner.width = 16;
227
228        // Add multiple lines of text
229        banner.add_text("Hello, ");
230        banner.add_text("World!");
231
232        let expected = "\u{1b}[37m┌────────────────┐\u{1b}[0m\r\n\u{1b}[37m│\u{1b}[0m\u{1b}[31mHello, \u{1b}[0m         \u{1b}[37m│\u{1b}[0m\r\n\u{1b}[37m│\u{1b}[0m\u{1b}[31mWorld!\u{1b}[0m          \u{1b}[37m│\u{1b}[0m\r\n\u{1b}[37m└────────────────┘\u{1b}[0m\r\n";
233        assert_eq!(expected, banner.assemble());
234    }
235}