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}