anstyle_roff/
styled_str.rs

1//! Provide tools for generating anstyle stylings from text
2
3use anstyle::{AnsiColor, Color as AColor, Effects, Style};
4use cansi::{v3::CategorisedSlice, Color, Intensity};
5
6/// Produce a stream of [`StyledStr`] from text that contains ansi escape sequences
7pub(crate) fn styled_stream(text: &str) -> impl Iterator<Item = StyledStr<'_>> {
8    let categorized = cansi::v3::categorise_text(text);
9    categorized.into_iter().map(|x| x.into())
10}
11
12/// Represents a Section of text, along with the desired styling for it
13#[derive(Debug, Default, Clone, Copy)]
14pub(crate) struct StyledStr<'text> {
15    pub(crate) text: &'text str,
16    pub(crate) style: Style,
17}
18
19impl<'text> From<CategorisedSlice<'text>> for StyledStr<'text> {
20    fn from(category: CategorisedSlice<'text>) -> Self {
21        let mut style = Style::new();
22        style = style
23            .fg_color(cansi_to_anstyle_color(category.fg))
24            .bg_color(cansi_to_anstyle_color(category.bg));
25
26        let effects = create_effects(&category);
27        style = style.effects(effects);
28
29        Self {
30            text: category.text,
31            style,
32        }
33    }
34}
35
36fn create_effects(category: &CategorisedSlice<'_>) -> Effects {
37    Effects::new()
38        .set(Effects::ITALIC, category.italic.unwrap_or(false))
39        .set(Effects::BLINK, category.blink.unwrap_or(false))
40        .set(Effects::INVERT, category.reversed.unwrap_or(false))
41        .set(Effects::HIDDEN, category.hidden.unwrap_or(false))
42        .set(
43            Effects::STRIKETHROUGH,
44            category.strikethrough.unwrap_or(false),
45        )
46        .set(Effects::UNDERLINE, category.underline.unwrap_or(false))
47        .set(Effects::BOLD, is_bold(category.intensity))
48        .set(Effects::DIMMED, is_faint(category.intensity))
49}
50
51fn is_bold(intensity: Option<Intensity>) -> bool {
52    matches!(intensity, Some(Intensity::Bold))
53}
54
55fn is_faint(intensity: Option<Intensity>) -> bool {
56    matches!(intensity, Some(Intensity::Faint))
57}
58
59fn cansi_to_anstyle_color(color: Option<Color>) -> Option<AColor> {
60    match color {
61        Some(Color::Black) => Some(AColor::Ansi(AnsiColor::Black)),
62        Some(Color::Red) => Some(AColor::Ansi(AnsiColor::Red)),
63        Some(Color::Green) => Some(AColor::Ansi(AnsiColor::Green)),
64        Some(Color::Yellow) => Some(AColor::Ansi(AnsiColor::Yellow)),
65        Some(Color::Blue) => Some(AColor::Ansi(AnsiColor::Blue)),
66        Some(Color::Magenta) => Some(AColor::Ansi(AnsiColor::Magenta)),
67        Some(Color::Cyan) => Some(AColor::Ansi(AnsiColor::Cyan)),
68        Some(Color::White) => Some(AColor::Ansi(AnsiColor::White)),
69        Some(Color::BrightBlack) => Some(AColor::Ansi(AnsiColor::BrightBlack)),
70        Some(Color::BrightRed) => Some(AColor::Ansi(AnsiColor::BrightRed)),
71        Some(Color::BrightGreen) => Some(AColor::Ansi(AnsiColor::BrightGreen)),
72        Some(Color::BrightYellow) => Some(AColor::Ansi(AnsiColor::BrightYellow)),
73        Some(Color::BrightBlue) => Some(AColor::Ansi(AnsiColor::BrightBlue)),
74        Some(Color::BrightMagenta) => Some(AColor::Ansi(AnsiColor::BrightMagenta)),
75        Some(Color::BrightCyan) => Some(AColor::Ansi(AnsiColor::BrightCyan)),
76        Some(Color::BrightWhite) => Some(AColor::Ansi(AnsiColor::BrightWhite)),
77        None => None,
78    }
79}
80
81#[cfg(test)]
82mod tests {
83
84    use super::*;
85
86    /// Creates a [`CategorisedSlice`] for Testing
87    ///
88    /// ```rust
89    /// styled_str!(Text, [Color:COLOR_SET] [Intensity:INTENSITY_SET] [Effects:EFFECTS_SET])
90    /// ```
91    ///
92    /// Where:
93    ///     `COLOR_SET={fg|bg}:<cansi::Color>`
94    ///     `INTENSITY_SET=<cansi::Intensity>`
95    ///     `EFFECTS_SET={"underline"|"italic"|"blink"|"reversed"|"strikethrough"|"hidden"};+`
96    macro_rules! styled_str {
97        ($text: literal, $(Color:$color_key:literal:$color_val:expr;)* $(Intensity:$intensity:expr;)? $(Effects:$($key:literal;)+)? ) => {
98            {
99            let mut cat_text = CategorisedSlice {
100                text: $text,
101                start: 0,
102                end: 5,
103                fg: None,
104                bg: None,
105                intensity: Some(Intensity::Normal),
106                italic: None,
107                underline: None,
108                blink: None,
109                reversed: None,
110                strikethrough: None,
111                hidden: None,
112            };
113
114            $(
115            match $color_key {
116                "fg" => cat_text.fg = Some($color_val),
117                "bg" => cat_text.bg = Some($color_val),
118                _ => panic!("Not A Valid key for color")
119            };
120            )*
121            $(
122                cat_text.intensity = Some($intensity);
123            )?
124            $($(
125            match $key {
126                "underline" => cat_text.underline = Some(true),
127                "italic" => cat_text.italic= Some(true),
128                "blink" => cat_text.blink= Some(true),
129                "reversed" => cat_text.reversed = Some(true),
130                "strikethrough" => cat_text.strikethrough= Some(true),
131                "hidden" => cat_text.hidden= Some(true),
132                _ => panic!("Not A Valid key for effects")
133            };
134            )+)?
135            cat_text
136        }}
137    }
138
139    #[test]
140    fn from_categorized_underlined() {
141        let categorised = styled_str!("Hello", Effects:"underline";);
142        let styled_str: StyledStr<'_> = categorised.into();
143        assert!(styled_str.style.get_effects().contains(Effects::UNDERLINE));
144    }
145
146    #[test]
147    fn from_categorized_underlined_striketrhough() {
148        let categorised = styled_str!("Hello", Effects:"underline";"strikethrough";);
149        let styled_str: StyledStr<'_> = categorised.into();
150        assert!(styled_str.style.get_effects().contains(Effects::UNDERLINE));
151        assert!(styled_str
152            .style
153            .get_effects()
154            .contains(Effects::STRIKETHROUGH));
155    }
156
157    #[test]
158    fn from_categorized_blink() {
159        let categorised = styled_str!("Hello", Effects:"blink";);
160        let styled_str: StyledStr<'_> = categorised.into();
161        assert!(styled_str.style.get_effects().contains(Effects::BLINK));
162    }
163
164    #[test]
165    fn from_categorized_reversed() {
166        let categorised = styled_str!("Hello", Effects:"reversed";);
167        let styled_str: StyledStr<'_> = categorised.into();
168        assert!(styled_str.style.get_effects().contains(Effects::INVERT));
169    }
170
171    #[test]
172    fn from_categorized_strikthrough() {
173        let categorised = styled_str!("Hello", Effects:"strikethrough";);
174        let styled_str: StyledStr<'_> = categorised.into();
175        assert!(styled_str
176            .style
177            .get_effects()
178            .contains(Effects::STRIKETHROUGH));
179    }
180
181    #[test]
182    fn from_categorized_hidden() {
183        let categorised = styled_str!("Hello", Effects:"hidden";);
184        let styled_str: StyledStr<'_> = categorised.into();
185        assert!(styled_str.style.get_effects().contains(Effects::HIDDEN));
186    }
187
188    #[test]
189    fn from_categorized_bg() {
190        let categorised = styled_str!("Hello", Color:"bg":Color::Blue;);
191        let styled_str: StyledStr<'_> = categorised.into();
192        assert!(matches!(
193            styled_str.style.get_bg_color(),
194            Some(AColor::Ansi(AnsiColor::Blue))
195        ));
196    }
197
198    #[test]
199    fn from_categorized_fg() {
200        let categorised = styled_str!("Hello", Color:"fg":Color::Blue;);
201        let styled_str: StyledStr<'_> = categorised.into();
202        assert!(matches!(
203            styled_str.style.get_fg_color(),
204            Some(AColor::Ansi(AnsiColor::Blue))
205        ));
206    }
207
208    #[test]
209    fn from_categorized_bold() {
210        let categorised = styled_str!("Hello", Intensity:Intensity::Bold;);
211        let styled_str: StyledStr<'_> = categorised.into();
212        assert!(styled_str.style.get_effects().contains(Effects::BOLD));
213    }
214
215    #[test]
216    fn from_categorized_faint() {
217        let categorised = styled_str!("Hello", Intensity:Intensity::Faint;);
218        let styled_str: StyledStr<'_> = categorised.into();
219        assert!(styled_str.style.get_effects().contains(Effects::DIMMED));
220    }
221}