libtableformat/content/
content_style.rs

1use colored::Color;
2
3#[derive(Debug, Clone)]
4pub enum CellWidth {
5    // The cell width is fixed
6    Fixed(usize),
7    // The cell is always at least a minimum width
8    Minimum(usize),
9    // The cell takes on the width of its content
10    Content,
11}
12
13impl CellWidth {
14    pub fn default() -> CellWidth {
15        CellWidth::Content
16    }
17}
18
19/// Describes how content should be aligned.
20#[derive(Debug, Clone)]
21#[derive(PartialEq)]
22pub enum Alignment {
23    Left,
24    Center,
25    Right
26}
27
28impl Alignment {
29    fn from_token(token: char) -> Option<Alignment> {
30        match token {
31            '<' => Some(Alignment::Left),
32            '^' => Some(Alignment::Center),
33            '>' => Some(Alignment::Right),
34            _ => None
35        }
36    }
37}
38
39/// Describes whether content will wrap or truncate.
40#[derive(Debug, Clone)]
41pub enum Wrap {
42    /// Content will be truncated when over-width
43    Truncate,
44    // Content will wrap when over-width
45    Wrap
46}
47
48impl Wrap {
49    fn from_token(token: char) -> Option<Wrap> {
50        match token {
51            ';' => Some(Wrap::Wrap),
52            '.' => Some(Wrap::Truncate),
53            _ => None
54        }
55    }
56}
57
58#[allow(unused_macros)]
59#[macro_export]
60macro_rules! content_style {
61    ( $style:literal ) => {
62        ContentStyle::from_format($style)
63    }
64}
65
66/// Represents the style to apply to a line of content.
67#[derive(Debug, Clone)]
68pub struct ContentStyle {
69    pub foreground_color: Option<Color>,
70    pub background_color: Option<Color>,
71    pub alignment: Alignment,
72    pub wrap: Wrap,
73    pub width: CellWidth
74}
75
76impl ContentStyle {
77    #[must_use]
78    pub fn default() -> ContentStyle {
79        ContentStyle {
80            foreground_color: None,
81            background_color: None,
82            alignment: Alignment::Left,
83            wrap: Wrap::Truncate,
84            width: CellWidth::Content,
85        }
86    }
87
88    #[must_use]
89    pub fn new(
90        foreground_color: Option<Color>,
91        background_color: Option<Color>,
92        alignment: Alignment,
93        wrap: Wrap,
94        width: CellWidth
95    ) -> ContentStyle {
96        ContentStyle {
97            foreground_color,
98            background_color,
99            alignment,
100            wrap,
101            width,
102        }
103    }
104
105    /// Returns a `ContentStyle` from a format string.
106    ///
107    /// # Arguments
108    ///
109    /// * `format` - The format string to parse.
110    ///
111    /// # Panics
112    ///
113    /// If width specifiers are not well formatted.
114    #[must_use]
115    pub fn from_format(format: &str) -> ContentStyle {
116        // Start with defaults
117        let mut style = ContentStyle::default();
118
119        // Iterate tokens
120        let tokens: Vec<char> = format[1..format.len() - 1].chars().collect();
121        let mut token_ix = 0;
122        while token_ix < tokens.len() {
123            let token = tokens[token_ix];
124
125            // Foreground color
126            if let Some(color) = ContentStyle::color_from_token(token) {
127                style.foreground_color = Some(color)
128            }
129            // Alignment
130            if let Some(alignment) = Alignment::from_token(token) {
131                style.alignment = alignment
132            }
133            // Wrap
134            if let Some(wrap) = Wrap::from_token(token) {
135                style.wrap = wrap
136            }
137
138            // Background color (consumes two tokens)
139            if token == '-' {
140                // Avoid problem if - is last token (with no color code)
141                if tokens.len() > token_ix + 1 {
142                    style.background_color = 
143                        ContentStyle::color_from_token(tokens[token_ix + 1]);
144                    // Consume next token (to skip the background color code)
145                    token_ix += 1;
146                }
147            }
148            token_ix += 1;
149
150            // Width specifier (consumes until matching token)
151            if token == ':' {
152                // TODO: Clean up this logic (should be a common width fn)
153                if let Some(ix) = format[token_ix+1..=tokens.len()].find(':') {
154                    let width = format[token_ix+1..=token_ix+ix].parse::<usize>().unwrap();
155                    style.width = CellWidth::Fixed(width);
156                    token_ix += ix + 1;
157                }
158            }
159
160            // Width specifier (consumes until matching token)
161            if token == '|' {
162                // TODO: Clean up this logic (should be a common width fn)
163
164                if let Some(ix) = format[token_ix+1..=tokens.len()].find('|') {
165                        let width = format[token_ix+1..=token_ix+ix].parse::<usize>().unwrap();
166                        style.width = CellWidth::Minimum(width);
167                        token_ix += ix + 1;
168                }
169            }
170        }
171
172        style
173    }
174
175    fn color_from_token(
176        token: char
177    ) -> Option<Color> {
178        match token {
179            'w' => Some(Color::White),
180            'l' => Some(Color::Black),
181            'r' => Some(Color::Red),
182            'g' => Some(Color::Green),
183            'y' => Some(Color::Yellow),
184            'b' => Some(Color::Blue),
185            'm' => Some(Color::Magenta),
186            'c' => Some(Color::Cyan),
187            'W' => Some(Color::BrightWhite),
188            'L' => Some(Color::BrightBlack),
189            'R' => Some(Color::BrightRed),
190            'G' => Some(Color::BrightGreen),
191            'Y' => Some(Color::BrightYellow),
192            'B' => Some(Color::BrightBlue),
193            'M' => Some(Color::BrightMagenta),
194            'C' => Some(Color::BrightCyan),
195            _ => None,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use colored::Color;
204
205   #[test]
206   fn from_format_fixed_width() {
207        let style = ContentStyle::from_format("{c^;:15:}");
208   
209        let expected = 
210            ContentStyle {
211                foreground_color: Some(Color::Cyan),
212                background_color: None,
213                alignment: Alignment::Center,
214                wrap: Wrap::Wrap,
215                width: CellWidth::Fixed(15)
216            };
217
218        assert_eq!(
219            format!("{:?}", style),
220            format!("{:?}", expected)
221        );
222    }
223
224}