anstyle_roff/
styled_str.rs1use anstyle::{AnsiColor, Color as AColor, Effects, Style};
4use cansi::{v3::CategorisedSlice, Color, Intensity};
5
6pub(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#[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 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}