syntect_tui/lib.rs
1//! # syntect-ratatui
2//!
3//! `syntect-ratatui` is a lightweight toolset for converting from text stylised by
4//! [syntect](https://docs.rs/syntect/latest/syntect) into stylised text renderable in
5//! [ratatui](https://docs.rs/ratatui/latest/ratatui/) applications.
6//!
7//! Contributions welcome! Feel free to fork and submit a pull request.
8use custom_error::custom_error;
9
10custom_error! {
11 #[derive(PartialEq)]
12 pub SyntectTuiError
13 UnknownFontStyle { bits: u8 } = "Unable to convert syntect::FontStyle into ratatui::style::Modifier: unsupported bits ({bits}) value.",
14}
15
16/// Converts a line segment highlighed using [syntect::easy::HighlightLines::highlight_line](https://docs.rs/syntect/latest/syntect/easy/struct.HighlightLines.html#method.highlight_line) into a [ratatui::text::Span](https://docs.rs/ratatui/latest/ratatui/text/struct.Span.html).
17///
18/// Syntect colours are RGBA while Ratatui colours are RGB, so colour conversion is lossy. However, if a Syntect colour's alpha value is `0`, then we preserve this to some degree by returning a value of `None` for that colour (i.e. its colourless).
19///
20/// Additionally, [syntect::highlighting::Style](https://docs.rs/syntect/latest/syntect/highlighting/struct.Style.html) does not support underlines having a different color than the text it is applied to, unlike [ratatui::style::Style](https://docs.rs/ratatui/latest/ratatui/style/struct.Style.html).
21/// Because of this the `underline_color` is set to match the `foreground`.
22///
23/// # Examples
24/// Basic usage:
25/// ```
26/// let input_text = "hello";
27/// let input_style = syntect::highlighting::Style {
28/// foreground: syntect::highlighting::Color { r: 255, g: 0, b: 0, a: 255 },
29/// background: syntect::highlighting::Color { r: 0, g: 0, b: 0, a: 0 },
30/// font_style: syntect::highlighting::FontStyle::BOLD
31/// };
32/// let expected_style = ratatui::style::Style {
33/// fg: Some(ratatui::style::Color::Rgb(255, 0, 0)),
34/// bg: None,
35/// underline_color: Some(ratatui::style::Color::Rgb(255, 0, 0)),
36/// add_modifier: ratatui::style::Modifier::BOLD,
37/// sub_modifier: ratatui::style::Modifier::empty()
38/// };
39/// let expected_span = ratatui::text::Span::styled(input_text, expected_style);
40/// let actual_span = syntect_tui::into_span((input_style, input_text)).unwrap();
41/// assert_eq!(expected_span, actual_span);
42/// ```
43///
44/// Here's a more complex example that builds upon syntect's own example for `HighlightLines`:
45/// ```
46/// use syntect::easy::HighlightLines;
47/// use syntect::parsing::SyntaxSet;
48/// use syntect::highlighting::{ThemeSet, Style};
49/// use syntect::util::LinesWithEndings;
50/// use syntect_tui::into_span;
51///
52/// let ps = SyntaxSet::load_defaults_newlines();
53/// let ts = ThemeSet::load_defaults();
54/// let syntax = ps.find_syntax_by_extension("rs").unwrap();
55/// let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
56/// let s = "pub struct Wow { hi: u64 }\nfn blah() -> u64 {}";
57/// for line in LinesWithEndings::from(s) { // LinesWithEndings enables use of newlines mode
58/// let line_spans: Vec<ratatui::text::Span> =
59/// h.highlight_line(line, &ps)
60/// .unwrap()
61/// .into_iter()
62/// .filter_map(|segment| into_span(segment).ok())
63/// .collect();
64/// let spans = ratatui::text::Line::from(line_spans);
65/// print!("{:?}", spans);
66/// }
67///
68/// ```
69///
70/// # Errors
71/// Can return `SyntectTuiError::UnknownFontStyle` if the input [FontStyle](https://docs.rs/syntect/latest/syntect/highlighting/struct.FontStyle.html) is not supported.
72///
73/// All explicit compositions of `BOLD`, `ITALIC` & `UNDERLINE` are supported, however, implicit bitflag coercions are not. For example, even though `FontStyle::from_bits(3)` is coerced to `Some(FontStyle::BOLD | FontStyle::ITALIC)`, we ignore this result as it would be a pain to handle all implicit coercions.
74pub fn into_span<'a>(
75 (style, content): (syntect::highlighting::Style, &'a str),
76) -> Result<ratatui::text::Span<'a>, SyntectTuiError> {
77 Ok(ratatui::text::Span::styled(
78 String::from(content),
79 translate_style(style)?,
80 ))
81}
82
83/// Converts a
84/// [syntect::highlighting::Style](https://docs.rs/syntect/latest/syntect/highlighting/struct.Style.html)
85/// into a [ratatui::style::Style](https://docs.rs/ratatui/latest/ratatui/style/struct.Style.html).
86///
87/// Syntect colours are RGBA while Ratatui colours are RGB, so colour conversion is lossy. However, if a Syntect colour's alpha value is `0`, then we preserve this to some degree by returning a value of `None` for that colour (i.e. its colourless).
88///
89/// # Examples
90/// Basic usage:
91/// ```
92/// let input = syntect::highlighting::Style {
93/// foreground: syntect::highlighting::Color { r: 255, g: 0, b: 0, a: 255 },
94/// background: syntect::highlighting::Color { r: 0, g: 0, b: 0, a: 0 },
95/// font_style: syntect::highlighting::FontStyle::BOLD
96/// };
97/// let expected = ratatui::style::Style {
98/// fg: Some(ratatui::style::Color::Rgb(255, 0, 0)),
99/// bg: None,
100/// underline_color: Some(ratatui::style::Color::Rgb(255, 0, 0)),
101/// add_modifier: ratatui::style::Modifier::BOLD,
102/// sub_modifier: ratatui::style::Modifier::empty()
103/// };
104/// let actual = syntect_tui::translate_style(input).unwrap();
105/// assert_eq!(expected, actual);
106/// ```
107/// # Errors
108/// Can return `SyntectTuiError::UnknownFontStyle` if the input [FontStyle](https://docs.rs/syntect/latest/syntect/highlighting/struct.FontStyle.html) is not supported.
109///
110/// All explicit compositions of `BOLD`, `ITALIC` & `UNDERLINE` are supported, however, implicit bitflag coercions are not. For example, even though `FontStyle::from_bits(3)` is coerced to `Some(FontStyle::BOLD | FontStyle::ITALIC)`, we ignore this result as it would be a pain to handle all implicit coercions.
111pub fn translate_style(
112 syntect_style: syntect::highlighting::Style,
113) -> Result<ratatui::style::Style, SyntectTuiError> {
114 Ok(ratatui::style::Style {
115 fg: translate_colour(syntect_style.foreground),
116 bg: translate_colour(syntect_style.background),
117 underline_color: translate_colour(syntect_style.foreground),
118 add_modifier: translate_font_style(syntect_style.font_style)?,
119 sub_modifier: ratatui::style::Modifier::empty(),
120 })
121}
122
123/// Converts a
124/// [syntect::highlighting::Color](https://docs.rs/syntect/latest/syntect/highlighting/struct.Color.html)
125/// into a [ratatui::style::Color](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html).
126///
127///
128/// # Examples
129/// Basic usage:
130/// ```
131/// let input = syntect::highlighting::Color { r: 255, g: 0, b: 0, a: 255 };
132/// let expected = Some(ratatui::style::Color::Rgb(255, 0, 0));
133/// let actual = syntect_tui::translate_colour(input);
134/// assert_eq!(expected, actual);
135/// ```
136/// Syntect colours are RGBA while Ratatui colours are RGB, so colour conversion is lossy. However, if a Syntect colour's alpha value is `0`, then we preserve this to some degree by returning a value of `None` for that colour (i.e. colourless):
137/// ```
138/// assert_eq!(
139/// None,
140/// syntect_tui::translate_colour(syntect::highlighting::Color { r: 255, g: 0, b: 0, a: 0 })
141/// );
142/// ```
143pub fn translate_colour(
144 syntect_color: syntect::highlighting::Color,
145) -> Option<ratatui::style::Color> {
146 match syntect_color {
147 syntect::highlighting::Color { r, g, b, a } if a > 0 => {
148 Some(ratatui::style::Color::Rgb(r, g, b))
149 }
150 _ => None,
151 }
152}
153
154/// Converts a
155/// [syntect::highlighting::FontStyle](https://docs.rs/syntect/latest/syntect/highlighting/struct.FontStyle.html)
156/// into a [ratatui::style::Modifier](https://docs.rs/ratatui/latest/ratatui/style/struct.Modifier.html).
157///
158///
159/// # Examples
160/// Basic usage:
161/// ```
162/// let input = syntect::highlighting::FontStyle::BOLD | syntect::highlighting::FontStyle::ITALIC;
163/// let expected = ratatui::style::Modifier::BOLD | ratatui::style::Modifier::ITALIC;
164/// let actual = syntect_tui::translate_font_style(input).unwrap();
165/// assert_eq!(expected, actual);
166/// ```
167/// # Errors
168/// Can return `SyntectTuiError::UnknownFontStyle` if the input [FontStyle](https://docs.rs/syntect/latest/syntect/highlighting/struct.FontStyle.html) is not supported.
169///
170/// All explicit compositions of `BOLD`, `ITALIC` & `UNDERLINE` are supported, however, implicit bitflag coercions are not. For example, even though `FontStyle::from_bits(3)` is coerced to `Some(FontStyle::BOLD | FontStyle::ITALIC)`, we ignore this result as it would be a pain to handle all implicit coercions.
171pub fn translate_font_style(
172 syntect_font_style: syntect::highlighting::FontStyle,
173) -> Result<ratatui::style::Modifier, SyntectTuiError> {
174 use ratatui::style::Modifier;
175 use syntect::highlighting::FontStyle;
176 match syntect_font_style {
177 x if x == FontStyle::empty() => Ok(Modifier::empty()),
178 x if x == FontStyle::BOLD => Ok(Modifier::BOLD),
179 x if x == FontStyle::ITALIC => Ok(Modifier::ITALIC),
180 x if x == FontStyle::UNDERLINE => Ok(Modifier::UNDERLINED),
181 x if x == FontStyle::BOLD | FontStyle::ITALIC => Ok(Modifier::BOLD | Modifier::ITALIC),
182 x if x == FontStyle::BOLD | FontStyle::UNDERLINE => {
183 Ok(Modifier::BOLD | Modifier::UNDERLINED)
184 }
185 x if x == FontStyle::ITALIC | FontStyle::UNDERLINE => {
186 Ok(Modifier::ITALIC | Modifier::UNDERLINED)
187 }
188 x if x == FontStyle::BOLD | FontStyle::ITALIC | FontStyle::UNDERLINE => {
189 Ok(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED)
190 }
191 unknown => Err(SyntectTuiError::UnknownFontStyle {
192 bits: unknown.bits(),
193 }),
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use rstest::*;
200
201 use super::*;
202 use ratatui::style::Modifier;
203 use ratatui::text::Span;
204 use syntect::highlighting::{Color as SyntectColour, FontStyle, Style as SyntectStyle};
205
206 fn fake_syntect_colour(r: u8, g: u8, b: u8, a: u8) -> SyntectColour {
207 SyntectColour { r, g, b, a }
208 }
209
210 #[test]
211 fn can_convert_to_span() {
212 let (r, g, b) = (012_u8, 123_u8, 234_u8);
213 let style = SyntectStyle {
214 font_style: FontStyle::UNDERLINE,
215 foreground: fake_syntect_colour(r, g, b, 128),
216 background: fake_syntect_colour(g, b, r, 128),
217 };
218 let content = "syntax";
219 let expected = Ok(Span {
220 content: std::borrow::Cow::Owned(String::from(content)),
221 style: ratatui::style::Style {
222 fg: Some(ratatui::style::Color::Rgb(r, g, b)),
223 bg: Some(ratatui::style::Color::Rgb(g, b, r)),
224 underline_color: Some(ratatui::style::Color::Rgb(r, g, b)),
225 add_modifier: Modifier::UNDERLINED,
226 sub_modifier: Modifier::empty(),
227 },
228 });
229 let actual = into_span((style, content));
230 assert_eq!(expected, actual);
231 }
232
233 #[test]
234 fn translate_style_ok() {
235 let (r, g, b) = (012_u8, 123_u8, 234_u8);
236 let input = SyntectStyle {
237 font_style: FontStyle::UNDERLINE,
238 foreground: fake_syntect_colour(r, g, b, 128),
239 background: fake_syntect_colour(g, b, r, 128),
240 };
241 let expected = Ok(ratatui::style::Style::default()
242 .fg(ratatui::style::Color::Rgb(r, g, b))
243 .bg(ratatui::style::Color::Rgb(g, b, r))
244 .underline_color(ratatui::style::Color::Rgb(r, g, b))
245 .add_modifier(Modifier::UNDERLINED));
246 let actual = translate_style(input);
247 assert_eq!(expected, actual);
248 }
249
250 #[test]
251 fn translate_style_err() {
252 let colour = fake_syntect_colour(012, 123, 234, 128);
253 let input = SyntectStyle {
254 font_style: unsafe { FontStyle::from_bits_unchecked(254) },
255 foreground: colour.to_owned(),
256 background: colour,
257 };
258 let expected = Err(SyntectTuiError::UnknownFontStyle { bits: 254 });
259 let actual = translate_style(input);
260 assert_eq!(expected, actual);
261 }
262
263 #[rstest]
264 #[case::with_alpha(
265 fake_syntect_colour(012, 123, 234, 128),
266 Some(ratatui::style::Color::Rgb(012, 123, 234))
267 )]
268 #[case::without_alpha(fake_syntect_colour(012, 123, 234, 0), None)]
269 fn check_translate_colour(
270 #[case] input: SyntectColour,
271 #[case] expected: Option<ratatui::style::Color>,
272 ) {
273 assert_eq!(expected, translate_colour(input));
274 }
275
276 #[rstest]
277 #[case::empty(FontStyle::empty(), Ok(Modifier::empty()))]
278 #[case::bold(FontStyle::BOLD, Ok(Modifier::BOLD))]
279 #[case::italic(FontStyle::ITALIC, Ok(Modifier::ITALIC))]
280 #[case::underline(FontStyle::UNDERLINE, Ok(Modifier::UNDERLINED))]
281 #[case::bold_italic(FontStyle::BOLD | FontStyle::ITALIC, Ok(Modifier::BOLD | Modifier::ITALIC))]
282 #[case::bold_underline(FontStyle::BOLD | FontStyle::UNDERLINE, Ok(Modifier::BOLD | Modifier::UNDERLINED))]
283 #[case::italic_underline(FontStyle::ITALIC | FontStyle::UNDERLINE, Ok(Modifier::ITALIC | Modifier::UNDERLINED))]
284 #[case::bold_italic_underline(
285 FontStyle::BOLD | FontStyle::ITALIC | FontStyle::UNDERLINE,
286 Ok(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED)
287 )]
288 #[case::err(
289 unsafe { FontStyle::from_bits_unchecked(254) } ,
290 Err(SyntectTuiError::UnknownFontStyle { bits: 254 })
291 )]
292 fn check_translate_font_style(
293 #[case] input: FontStyle,
294 #[case] expected: Result<Modifier, SyntectTuiError>,
295 ) {
296 let actual = translate_font_style(input);
297 assert_eq!(expected, actual);
298 }
299}